@principles/pd-cli 1.145.2 → 1.146.1
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/README.md +11 -2
- package/dist/commands/build-trajectory-evidence.d.ts +25 -6
- package/dist/commands/build-trajectory-evidence.d.ts.map +1 -1
- package/dist/commands/build-trajectory-evidence.js +174 -114
- package/dist/commands/build-trajectory-evidence.js.map +1 -1
- package/dist/commands/console.d.ts +13 -0
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +104 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +212 -17
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/build-trajectory-evidence.ts +211 -122
- package/src/commands/console.ts +105 -0
- package/src/commands/pain-record.ts +224 -16
- package/src/index.ts +1 -1
- package/tests/commands/build-trajectory-evidence.test.ts +226 -161
- package/tests/commands/console-open.test.ts +175 -0
- package/tests/commands/pain-record-async.test.ts +36 -30
- package/tests/commands/pain-record-session-parser.test.ts +127 -0
- package/tests/commands/pain-record.test.ts +205 -37
|
@@ -569,6 +569,28 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
569
569
|
fs.writeFileSync(path.join(consoleDir, 'package.json'), JSON.stringify({ name: 'fake-pd-console', version: '0.0.0', type: 'module' }, null, 2));
|
|
570
570
|
// EP-06 regression guard: dist/web/index.html must exist (PR #1169 fix)
|
|
571
571
|
fs.writeFileSync(path.join(consoleDir, 'dist', 'web', 'index.html'), '<!DOCTYPE html><html></html>');
|
|
572
|
+
// Runtime dependency integrity check: fake console needs minimal node_modules
|
|
573
|
+
// slots so the launcher passes the integrity gate and reaches spawn/health logic.
|
|
574
|
+
const nm = path.join(consoleDir, 'node_modules');
|
|
575
|
+
// principles-disciple: server imports principles-disciple/governance-audit
|
|
576
|
+
fs.mkdirSync(path.join(nm, 'principles-disciple', 'dist'), { recursive: true });
|
|
577
|
+
fs.writeFileSync(path.join(nm, 'principles-disciple', 'package.json'), JSON.stringify({ name: 'x', version: '0.0.0' }, null, 2));
|
|
578
|
+
fs.writeFileSync(path.join(nm, 'principles-disciple', 'dist', 'governance-audit.js'), 'export {};\n');
|
|
579
|
+
// @principles/core: server imports TWO subpaths
|
|
580
|
+
// @principles/core/principle-tree-ledger → dist/principle-tree-ledger.js
|
|
581
|
+
// @principles/core/runtime-v2 → dist/runtime-v2/index.js
|
|
582
|
+
fs.mkdirSync(path.join(nm, '@principles', 'core', 'dist', 'runtime-v2'), { recursive: true });
|
|
583
|
+
fs.writeFileSync(path.join(nm, '@principles', 'core', 'package.json'), JSON.stringify({ name: 'x', version: '0.0.0' }, null, 2));
|
|
584
|
+
fs.writeFileSync(path.join(nm, '@principles', 'core', 'dist', 'principle-tree-ledger.js'), 'export {};\n');
|
|
585
|
+
fs.writeFileSync(path.join(nm, '@principles', 'core', 'dist', 'runtime-v2', 'index.js'), 'export {};\n');
|
|
586
|
+
// @principles/host-runtime: uses root entry dist/index.js
|
|
587
|
+
fs.mkdirSync(path.join(nm, '@principles', 'host-runtime', 'dist'), { recursive: true });
|
|
588
|
+
fs.writeFileSync(path.join(nm, '@principles', 'host-runtime', 'package.json'), JSON.stringify({ name: 'x', version: '0.0.0' }, null, 2));
|
|
589
|
+
fs.writeFileSync(path.join(nm, '@principles', 'host-runtime', 'dist', 'index.js'), 'export {};\n');
|
|
590
|
+
// @principles/install-layout: uses root entry dist/index.js
|
|
591
|
+
fs.mkdirSync(path.join(nm, '@principles', 'install-layout', 'dist'), { recursive: true });
|
|
592
|
+
fs.writeFileSync(path.join(nm, '@principles', 'install-layout', 'package.json'), JSON.stringify({ name: 'x', version: '0.0.0' }, null, 2));
|
|
593
|
+
fs.writeFileSync(path.join(nm, '@principles', 'install-layout', 'dist', 'index.js'), 'export {};\n');
|
|
572
594
|
fs.writeFileSync(path.join(consoleDir, 'dist', 'server.js'), `
|
|
573
595
|
import http from 'node:http';
|
|
574
596
|
const args = process.argv.slice(2);
|
|
@@ -710,6 +732,48 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
710
732
|
}
|
|
711
733
|
}, 20_000);
|
|
712
734
|
|
|
735
|
+
it('reuses a healthy running Console even when local dependency slots are broken (reuse-before-integrity contract)', async () => {
|
|
736
|
+
// Regression for the control-flow contract: the integrity gate must only
|
|
737
|
+
// run on the fresh-spawn branch. If a healthy Console is already running,
|
|
738
|
+
// broken on-disk node_modules (dangling junction / empty shell left by an
|
|
739
|
+
// interrupted update) must NOT prevent reusing it — its modules are loaded
|
|
740
|
+
// in memory. This test breaks the @principles/core slot on disk, then
|
|
741
|
+
// verifies the CLI still returns status "reused" (never hits the gate).
|
|
742
|
+
const nm = path.join(
|
|
743
|
+
process.env.__PD_CONSOLE_TEST_FAKE_HOME ?? '',
|
|
744
|
+
'.openclaw', 'extensions', 'principles-disciple', 'console', 'node_modules',
|
|
745
|
+
);
|
|
746
|
+
// Simulate a broken install: remove the @principles/core dist entry entirely.
|
|
747
|
+
const coreDist = path.join(nm, '@principles', 'core', 'dist');
|
|
748
|
+
if (fs.existsSync(coreDist)) fs.rmSync(coreDist, { recursive: true, force: true });
|
|
749
|
+
|
|
750
|
+
const server = http.createServer((req, res) => {
|
|
751
|
+
if (req.url === '/api/health') {
|
|
752
|
+
res.statusCode = 200;
|
|
753
|
+
res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
res.statusCode = 404;
|
|
757
|
+
res.end();
|
|
758
|
+
});
|
|
759
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
|
|
760
|
+
const addr = server.address();
|
|
761
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
762
|
+
let run: CliJsonRun | undefined;
|
|
763
|
+
try {
|
|
764
|
+
run = await runPdUntilJson(
|
|
765
|
+
['console', 'open', '--workspace', tmp, '--port', String(addr.port), '--json', '--no-browser'],
|
|
766
|
+
workspaceRoot,
|
|
767
|
+
);
|
|
768
|
+
if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
|
|
769
|
+
expect(run.parsed.status).toBe('reused');
|
|
770
|
+
expect(run.parsed.reused).toBe(true);
|
|
771
|
+
} finally {
|
|
772
|
+
await teardownCliTree(run);
|
|
773
|
+
server.close();
|
|
774
|
+
}
|
|
775
|
+
}, 20_000);
|
|
776
|
+
|
|
713
777
|
it('pd console open --port 99999 --json returns a structured failure (invalid port)', () => {
|
|
714
778
|
const out = runPd(['console', 'open', '--workspace', tmp, '--port', '99999', '--json', '--no-browser'], workspaceRoot);
|
|
715
779
|
const parsed = JSON.parse(out);
|
|
@@ -1115,3 +1179,114 @@ function killTreeForce(child: childProcessModule.ChildProcess): void {
|
|
|
1115
1179
|
child.kill('SIGKILL');
|
|
1116
1180
|
} catch { /* already gone */ }
|
|
1117
1181
|
}
|
|
1182
|
+
|
|
1183
|
+
import { checkConsoleRuntimeDependencies } from '../../src/commands/console.js';
|
|
1184
|
+
|
|
1185
|
+
describe('checkConsoleRuntimeDependencies', () => {
|
|
1186
|
+
it('returns undefined when all deps are intact', () => {
|
|
1187
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'console-integ-'));
|
|
1188
|
+
try {
|
|
1189
|
+
// @principles/core: entries are dist/principle-tree-ledger.js + dist/runtime-v2/index.js
|
|
1190
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2'), { recursive: true });
|
|
1191
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'package.json'), '{}');
|
|
1192
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'principle-tree-ledger.js'), '');
|
|
1193
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2', 'index.js'), '');
|
|
1194
|
+
// @principles/host-runtime: entry is dist/index.js
|
|
1195
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime', 'dist'), { recursive: true });
|
|
1196
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime', 'package.json'), '{}');
|
|
1197
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime', 'dist', 'index.js'), '');
|
|
1198
|
+
// @principles/install-layout: entry is dist/index.js
|
|
1199
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist'), { recursive: true });
|
|
1200
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'package.json'), '{}');
|
|
1201
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist', 'index.js'), '');
|
|
1202
|
+
// principles-disciple: entry is dist/governance-audit.js
|
|
1203
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist'), { recursive: true });
|
|
1204
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'package.json'), '{}');
|
|
1205
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist', 'governance-audit.js'), '');
|
|
1206
|
+
|
|
1207
|
+
expect(checkConsoleRuntimeDependencies(tmp)).toBeUndefined();
|
|
1208
|
+
} finally {
|
|
1209
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('returns error when a dep is missing package.json', () => {
|
|
1214
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'console-integ-'));
|
|
1215
|
+
try {
|
|
1216
|
+
// Create all deps intact except host-runtime (no package.json)
|
|
1217
|
+
// core
|
|
1218
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2'), { recursive: true });
|
|
1219
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'package.json'), '{}');
|
|
1220
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'principle-tree-ledger.js'), '');
|
|
1221
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2', 'index.js'), '');
|
|
1222
|
+
// install-layout
|
|
1223
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist'), { recursive: true });
|
|
1224
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'package.json'), '{}');
|
|
1225
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist', 'index.js'), '');
|
|
1226
|
+
// principles-disciple
|
|
1227
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist'), { recursive: true });
|
|
1228
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'package.json'), '{}');
|
|
1229
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist', 'governance-audit.js'), '');
|
|
1230
|
+
// @principles/host-runtime without package.json — the broken-shell case
|
|
1231
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime', 'dist'), { recursive: true });
|
|
1232
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime', 'dist', 'index.js'), '');
|
|
1233
|
+
|
|
1234
|
+
const err = checkConsoleRuntimeDependencies(tmp);
|
|
1235
|
+
expect(err).toContain('host-runtime');
|
|
1236
|
+
expect(err).toContain('package.json');
|
|
1237
|
+
} finally {
|
|
1238
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it('returns error when a dep has package.json but no dist entry (empty shell)', () => {
|
|
1243
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'console-integ-'));
|
|
1244
|
+
try {
|
|
1245
|
+
// core intact
|
|
1246
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2'), { recursive: true });
|
|
1247
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'package.json'), '{}');
|
|
1248
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'principle-tree-ledger.js'), '');
|
|
1249
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2', 'index.js'), '');
|
|
1250
|
+
// install-layout intact
|
|
1251
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist'), { recursive: true });
|
|
1252
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'package.json'), '{}');
|
|
1253
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist', 'index.js'), '');
|
|
1254
|
+
// principles-disciple intact
|
|
1255
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist'), { recursive: true });
|
|
1256
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'package.json'), '{}');
|
|
1257
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist', 'governance-audit.js'), '');
|
|
1258
|
+
// host-runtime exists but has no dist/index.js (root entry missing)
|
|
1259
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime'));
|
|
1260
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'host-runtime', 'package.json'), '{}');
|
|
1261
|
+
|
|
1262
|
+
const err = checkConsoleRuntimeDependencies(tmp);
|
|
1263
|
+
expect(err).toContain('host-runtime');
|
|
1264
|
+
expect(err).toContain('dist/index.js');
|
|
1265
|
+
} finally {
|
|
1266
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it('returns error when a dep junction is dangling (directory missing)', () => {
|
|
1271
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'console-integ-'));
|
|
1272
|
+
try {
|
|
1273
|
+
// Create core, install-layout, principles-disciple; leave host-runtime missing
|
|
1274
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2'), { recursive: true });
|
|
1275
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'package.json'), '{}');
|
|
1276
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'principle-tree-ledger.js'), '');
|
|
1277
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'core', 'dist', 'runtime-v2', 'index.js'), '');
|
|
1278
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist'), { recursive: true });
|
|
1279
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'package.json'), '{}');
|
|
1280
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', '@principles', 'install-layout', 'dist', 'index.js'), '');
|
|
1281
|
+
fs.mkdirSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist'), { recursive: true });
|
|
1282
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'package.json'), '{}');
|
|
1283
|
+
fs.writeFileSync(path.join(tmp, 'node_modules', 'principles-disciple', 'dist', 'governance-audit.js'), '');
|
|
1284
|
+
|
|
1285
|
+
const err = checkConsoleRuntimeDependencies(tmp);
|
|
1286
|
+
expect(err).toContain('host-runtime');
|
|
1287
|
+
expect(err).toContain('package.json');
|
|
1288
|
+
} finally {
|
|
1289
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
@@ -31,36 +31,42 @@ vi.mock('../../src/commands/build-trajectory-evidence.js', () => ({
|
|
|
31
31
|
]),
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
|
-
vi.mock('@principles/core/runtime-v2', () =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
34
|
+
vi.mock('@principles/core/runtime-v2', async (importOriginal) => {
|
|
35
|
+
// PRI-642 review blocker 1: keep the REAL core evaluatePainIngress (the
|
|
36
|
+
// shared semantic authority) — only service/IO classes are mocked.
|
|
37
|
+
const actual = await importOriginal<typeof import('@principles/core/runtime-v2')>();
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
PainToPrincipleService: vi.fn().mockImplementation(function(this: Record<string, unknown>, opts: Record<string, unknown>) {
|
|
41
|
+
lastServiceOpts = opts;
|
|
42
|
+
return {
|
|
43
|
+
recordPain: vi.fn(async () => mockRecordPainResult),
|
|
44
|
+
};
|
|
45
|
+
}),
|
|
46
|
+
PrincipleTreeLedgerAdapter: vi.fn().mockImplementation(function() { return {}; }),
|
|
47
|
+
computeEffectivePdConfig: vi.fn().mockReturnValue({
|
|
48
|
+
runtimeKind: 'pi-ai',
|
|
49
|
+
provider: 'test-provider',
|
|
50
|
+
model: 'test-model',
|
|
51
|
+
apiKeyEnv: 'TEST_KEY',
|
|
52
|
+
timeoutMs: 300000,
|
|
53
|
+
agentId: 'main',
|
|
54
|
+
language: 'zh-CN',
|
|
55
|
+
warnings: [],
|
|
56
|
+
}),
|
|
57
|
+
resolveRuntimeConfig: vi.fn().mockReturnValue({
|
|
58
|
+
runtimeKind: 'pi-ai',
|
|
59
|
+
provider: 'test-provider',
|
|
60
|
+
model: 'test-model',
|
|
61
|
+
apiKeyEnv: 'TEST_KEY',
|
|
62
|
+
timeoutMs: 300000,
|
|
63
|
+
agentId: 'main',
|
|
64
|
+
}),
|
|
65
|
+
isRuntimeConfigError: vi.fn().mockReturnValue(false),
|
|
66
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
67
|
+
isFeatureEnabled: vi.fn().mockImplementation(() => mockIsFeatureEnabledReturn),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
64
70
|
|
|
65
71
|
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
66
72
|
loadPdConfig: vi.fn().mockReturnValue({
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-642 Scope A — real-Commander integration for `pd pain record --session`.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the BUILT pd CLI (dist/index.js) against a real temp workspace with a
|
|
5
|
+
* real trajectory.db, exercising the actual commander registration and the
|
|
6
|
+
* real typed acquisition path — not the in-process handler with mocks
|
|
7
|
+
* (cli-7-test-wiring; SPEC §13 "Real Commander parser/registration tests for
|
|
8
|
+
* --session and --json").
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
|
|
17
|
+
let tmpDir: string;
|
|
18
|
+
|
|
19
|
+
function createWorkspace(): void {
|
|
20
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-record-parser-'));
|
|
21
|
+
const stateDir = path.join(tmpDir, '.state');
|
|
22
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const db = new Database(path.join(stateDir, 'trajectory.db'));
|
|
25
|
+
db.exec("CREATE TABLE IF NOT EXISTS sessions (session_id TEXT PRIMARY KEY, started_at TEXT, updated_at TEXT)");
|
|
26
|
+
db.exec("CREATE TABLE IF NOT EXISTS assistant_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, sanitized_text TEXT, stop_reason TEXT, created_at TEXT NOT NULL)");
|
|
27
|
+
db.exec("CREATE TABLE IF NOT EXISTS user_turns (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, raw_excerpt TEXT, correction_detected INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL)");
|
|
28
|
+
db.exec("CREATE TABLE IF NOT EXISTS tool_calls (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, outcome TEXT NOT NULL, error_type TEXT, exit_code INTEGER, params_json TEXT NOT NULL DEFAULT '{}', result_preview TEXT, created_at TEXT NOT NULL)");
|
|
29
|
+
db.prepare('INSERT INTO sessions (session_id, started_at, updated_at) VALUES (?, ?, ?)')
|
|
30
|
+
.run('real-session-1', '2026-01-01T09:00:00Z', '2026-01-01T09:00:00Z');
|
|
31
|
+
db.prepare('INSERT INTO user_turns (session_id, raw_excerpt, correction_detected, created_at) VALUES (?, ?, ?, ?)')
|
|
32
|
+
.run('real-session-1', 'Please fix the output format', 1, '2026-01-01T09:59:00Z');
|
|
33
|
+
db.close();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CliRunResult {
|
|
37
|
+
status: number | null;
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resolves and boundary-validates the built pd CLI entry before spawning it. */
|
|
43
|
+
async function resolveBuiltCliEntry(): Promise<string> {
|
|
44
|
+
const packageRoot = path.resolve(process.cwd());
|
|
45
|
+
const entry = path.resolve(packageRoot, 'dist', 'index.js');
|
|
46
|
+
if (!entry.startsWith(`${packageRoot}${path.sep}`) || !fs.statSync(entry).isFile()) {
|
|
47
|
+
throw new Error(`built pd CLI entry not found or outside the package: ${entry}`);
|
|
48
|
+
}
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runBuiltCli(literalArgv: readonly string[]): Promise<CliRunResult> {
|
|
53
|
+
const { execFile } = await import('node:child_process');
|
|
54
|
+
const execFileAsync = promisify(execFile);
|
|
55
|
+
const entry = await resolveBuiltCliEntry();
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execFileAsync(process.execPath, [entry, ...literalArgv], {
|
|
58
|
+
cwd: tmpDir,
|
|
59
|
+
timeout: 30000,
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
env: { ...process.env, PD_WORKSPACE_DIR: tmpDir },
|
|
62
|
+
});
|
|
63
|
+
return { status: 0, stdout, stderr: '' };
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const e = err as { code?: number; stdout?: string; stderr?: string };
|
|
66
|
+
return { status: e.code ?? -1, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('pd pain record --session (real Commander + real trajectory.db)', () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
createWorkspace();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
try {
|
|
77
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore cleanup errors
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('--help registers and documents --session', async () => {
|
|
84
|
+
const result = await runBuiltCli(['pain', 'record', '--help']);
|
|
85
|
+
expect(result.status).toBe(0);
|
|
86
|
+
expect(result.stdout).toContain('--session');
|
|
87
|
+
}, 15_000);
|
|
88
|
+
|
|
89
|
+
it('fails with a single JSON object and reason session_not_found for a nonexistent session (SPEC 12.1.4)', async () => {
|
|
90
|
+
const result = await runBuiltCli([
|
|
91
|
+
'pain', 'record',
|
|
92
|
+
'--reason', 'parser test pain',
|
|
93
|
+
'--session', 'no-such-session',
|
|
94
|
+
'--workspace', tmpDir,
|
|
95
|
+
'--json',
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// Failed validation exits non-zero.
|
|
99
|
+
expect(result.status).not.toBe(0);
|
|
100
|
+
// cli-1: stdout is exactly one JSON object.
|
|
101
|
+
const trimmed = result.stdout.trim();
|
|
102
|
+
expect(trimmed.length).toBeGreaterThan(0);
|
|
103
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
104
|
+
expect(parsed.status).toBe('failed');
|
|
105
|
+
expect(parsed.reason).toBe('session_not_found');
|
|
106
|
+
expect(typeof parsed.nextAction).toBe('string');
|
|
107
|
+
}, 15_000);
|
|
108
|
+
|
|
109
|
+
it('accepts --session for a session that exists in the trajectory (validation passes, submission proceeds)', async () => {
|
|
110
|
+
const result = await runBuiltCli([
|
|
111
|
+
'pain', 'record',
|
|
112
|
+
'--reason', 'parser test pain with real session',
|
|
113
|
+
'--session', 'real-session-1',
|
|
114
|
+
'--workspace', tmpDir,
|
|
115
|
+
'--json',
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
// The session exists, so session validation must NOT reject it. The run
|
|
119
|
+
// may still fail later (no LLM runtime configured in this temp
|
|
120
|
+
// workspace) — the assertion is only about the validation stage.
|
|
121
|
+
const trimmed = result.stdout.trim();
|
|
122
|
+
expect(trimmed.length).toBeGreaterThan(0);
|
|
123
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
124
|
+
expect(parsed.reason).not.toBe('session_not_found');
|
|
125
|
+
expect(parsed.reason).not.toBe('empty_trajectory');
|
|
126
|
+
}, 15_000);
|
|
127
|
+
});
|