gsd-pi 2.76.0-dev.76f9a2dc5 → 2.76.0-dev.97807402
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-cli-check.js +32 -3
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
- package/dist/resources/extensions/gsd/auto/phases.js +14 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-start.js +27 -18
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
- package/dist/resources/extensions/gsd/auto.js +13 -17
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/error-classifier.js +10 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +115 -7
- package/dist/resources/extensions/gsd/guided-flow.js +189 -0
- package/dist/resources/extensions/gsd/health-widget.js +4 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/model-router.js +36 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +9 -3
- package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
- package/dist/resources/extensions/gsd/token-counter.js +22 -5
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/dist/resources/skills/write-docs/SKILL.md +2 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
- package/packages/mcp-server/dist/remote-questions.js +732 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +18 -1
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -1
- package/packages/mcp-server/src/remote-questions.test.ts +294 -0
- package/packages/mcp-server/src/remote-questions.ts +916 -0
- package/packages/mcp-server/src/server.ts +19 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.test.json +19 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +16 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
- package/packages/pi-ai/src/providers/simple-options.ts +17 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
- package/src/resources/extensions/gsd/auto/phases.ts +14 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
- package/src/resources/extensions/gsd/auto-start.ts +29 -19
- package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
- package/src/resources/extensions/gsd/auto.ts +12 -17
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/error-classifier.ts +10 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +122 -7
- package/src/resources/extensions/gsd/guided-flow.ts +221 -0
- package/src/resources/extensions/gsd/health-widget.ts +3 -1
- package/src/resources/extensions/gsd/journal.ts +2 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/model-router.ts +42 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
- package/src/resources/extensions/gsd/preferences-types.ts +46 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +13 -2
- package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
- package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/token-counter.ts +22 -5
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/src/resources/skills/write-docs/SKILL.md +2 -1
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildSnapshot,
|
|
9
|
+
readCompactionSnapshot,
|
|
10
|
+
writeCompactionSnapshot,
|
|
11
|
+
DEFAULT_SNAPSHOT_BYTES,
|
|
12
|
+
} from '../compaction-snapshot.ts';
|
|
13
|
+
import { closeDatabase, openDatabase } from '../gsd-db.ts';
|
|
14
|
+
import { createMemory } from '../memory-store.ts';
|
|
15
|
+
import { executeResume } from '../tools/resume-tool.ts';
|
|
16
|
+
|
|
17
|
+
function freshBase(): string {
|
|
18
|
+
return mkdtempSync(join(tmpdir(), 'gsd-snap-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanup(dir: string): void {
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('buildSnapshot: renders memories, exec history, and active context', () => {
|
|
26
|
+
const snap = buildSnapshot({
|
|
27
|
+
generatedAt: new Date('2026-04-20T12:00:00.000Z'),
|
|
28
|
+
activeContext: 'M001 / S01 / T01 — wire gsd_exec',
|
|
29
|
+
memories: [
|
|
30
|
+
{ id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
|
|
31
|
+
source_unit_type: null, source_unit_id: null, created_at: '', updated_at: '',
|
|
32
|
+
superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null },
|
|
33
|
+
],
|
|
34
|
+
execHistory: [
|
|
35
|
+
{
|
|
36
|
+
id: 'abc',
|
|
37
|
+
runtime: 'bash',
|
|
38
|
+
purpose: 'count TODOs',
|
|
39
|
+
started_at: '', finished_at: '', duration_ms: 10,
|
|
40
|
+
exit_code: 0, signal: null, timed_out: false,
|
|
41
|
+
stdout_bytes: 1, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false,
|
|
42
|
+
stdout_path: '/tmp/abc.stdout', stderr_path: '/tmp/abc.stderr', meta_path: '/tmp/abc.meta.json',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
assert.match(snap, /Active context/);
|
|
47
|
+
assert.match(snap, /M001 \/ S01 \/ T01/);
|
|
48
|
+
assert.match(snap, /FTS5 needs Porter tokenizer/);
|
|
49
|
+
assert.match(snap, /\[abc\] bash exit:0 — count TODOs/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
|
|
53
|
+
const longMemories = Array.from({ length: 50 }, (_v, i) => ({
|
|
54
|
+
id: `MEM${String(i).padStart(3, '0')}`,
|
|
55
|
+
category: 'gotcha',
|
|
56
|
+
content: 'x'.repeat(200),
|
|
57
|
+
confidence: 0.8,
|
|
58
|
+
source_unit_type: null,
|
|
59
|
+
source_unit_id: null,
|
|
60
|
+
created_at: '',
|
|
61
|
+
updated_at: '',
|
|
62
|
+
superseded_by: null,
|
|
63
|
+
hit_count: 0,
|
|
64
|
+
scope: 'project',
|
|
65
|
+
seq: i,
|
|
66
|
+
tags: [] as string[],
|
|
67
|
+
structured_fields: null,
|
|
68
|
+
}));
|
|
69
|
+
const snap = buildSnapshot(
|
|
70
|
+
{ generatedAt: new Date(), memories: longMemories, execHistory: [] },
|
|
71
|
+
{ maxBytes: 512, maxMemories: 50 },
|
|
72
|
+
);
|
|
73
|
+
assert.ok(Buffer.byteLength(snap, 'utf-8') <= 512, 'should respect cap');
|
|
74
|
+
assert.match(snap, /\[truncated\]/, 'should include truncation marker');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('buildSnapshot: handles empty state with an explanatory placeholder', () => {
|
|
78
|
+
const snap = buildSnapshot({ generatedAt: new Date(), memories: [], execHistory: [] });
|
|
79
|
+
assert.match(snap, /_No durable memories/);
|
|
80
|
+
assert.ok(Buffer.byteLength(snap, 'utf-8') <= DEFAULT_SNAPSHOT_BYTES);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('writeCompactionSnapshot + readCompactionSnapshot + executeResume: end-to-end', () => {
|
|
84
|
+
const base = freshBase();
|
|
85
|
+
try {
|
|
86
|
+
openDatabase(':memory:');
|
|
87
|
+
createMemory({ category: 'architecture', content: 'Single-writer DB through gsd-db.ts', confidence: 0.95 });
|
|
88
|
+
createMemory({ category: 'convention', content: 'Prefer typed helpers over raw SQL', confidence: 0.9 });
|
|
89
|
+
|
|
90
|
+
const out = writeCompactionSnapshot(base, { activeContext: 'M099 resume check' });
|
|
91
|
+
assert.ok(out.path.endsWith('last-snapshot.md'));
|
|
92
|
+
assert.ok(out.bytes > 0);
|
|
93
|
+
assert.equal(out.memories, 2);
|
|
94
|
+
|
|
95
|
+
const contents = readCompactionSnapshot(base);
|
|
96
|
+
assert.ok(contents);
|
|
97
|
+
assert.match(contents!, /Single-writer DB through gsd-db\.ts/);
|
|
98
|
+
assert.match(contents!, /M099 resume check/);
|
|
99
|
+
|
|
100
|
+
const tool = executeResume({}, { baseDir: base });
|
|
101
|
+
assert.ok(!tool.isError);
|
|
102
|
+
assert.equal(tool.details.found, true);
|
|
103
|
+
assert.match(tool.content[0].text, /Single-writer DB through gsd-db\.ts/);
|
|
104
|
+
|
|
105
|
+
// also verify the file content matches (without trailing newline)
|
|
106
|
+
const raw = readFileSync(out.path, 'utf-8');
|
|
107
|
+
assert.ok(raw.endsWith('\n'));
|
|
108
|
+
} finally {
|
|
109
|
+
closeDatabase();
|
|
110
|
+
cleanup(base);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('executeResume: reports friendly empty state when no snapshot exists', () => {
|
|
115
|
+
const base = freshBase();
|
|
116
|
+
try {
|
|
117
|
+
const result = executeResume({}, { baseDir: base });
|
|
118
|
+
assert.equal(result.details.found, false);
|
|
119
|
+
assert.match(result.content[0].text, /No snapshot found/);
|
|
120
|
+
} finally {
|
|
121
|
+
cleanup(base);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ===');
|
|
|
125
125
|
|
|
126
126
|
const adapter = _getAdapter()!;
|
|
127
127
|
|
|
128
|
-
// Verify schema version is current (
|
|
128
|
+
// Verify schema version is current (v22 — quality_gates DDL fix)
|
|
129
129
|
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
130
|
-
assertEq(versionRow?.['v'],
|
|
130
|
+
assertEq(versionRow?.['v'], 22, 'schema version should be 22');
|
|
131
131
|
|
|
132
132
|
// Verify slices table has full_summary_md and full_uat_md columns
|
|
133
133
|
const cols = adapter.prepare("PRAGMA table_info(slices)").all();
|
|
@@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
|
|
|
109
109
|
|
|
110
110
|
const adapter = _getAdapter()!;
|
|
111
111
|
|
|
112
|
-
// Verify schema version is current (
|
|
112
|
+
// Verify schema version is current (v22 — quality_gates DDL fix)
|
|
113
113
|
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
114
|
-
assertEq(versionRow?.['v'],
|
|
114
|
+
assertEq(versionRow?.['v'], 22, 'schema version should be 22');
|
|
115
115
|
|
|
116
116
|
// Verify all 4 new tables exist
|
|
117
117
|
const tables = adapter.prepare(
|
|
@@ -768,6 +768,37 @@ test("runProviderChecks detects claude.cmd in PATH on Windows (#4503)", { skip:
|
|
|
768
768
|
});
|
|
769
769
|
});
|
|
770
770
|
|
|
771
|
+
test("runProviderChecks detects claude.exe in PATH on Windows (#4548)", { skip: process.platform !== "win32" }, () => {
|
|
772
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-cc-exe-home-")));
|
|
773
|
+
const binDir = join(tmpHome, "bin");
|
|
774
|
+
mkdirSync(binDir, { recursive: true });
|
|
775
|
+
|
|
776
|
+
// Some Windows installs ship a direct claude.exe binary (not a .cmd shim).
|
|
777
|
+
const fakeClaudeExe = join(binDir, "claude.exe");
|
|
778
|
+
writeFileSync(fakeClaudeExe, "");
|
|
779
|
+
|
|
780
|
+
withEnv({
|
|
781
|
+
HOME: tmpHome,
|
|
782
|
+
ANTHROPIC_API_KEY: undefined,
|
|
783
|
+
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
784
|
+
COPILOT_GITHUB_TOKEN: undefined,
|
|
785
|
+
GH_TOKEN: undefined,
|
|
786
|
+
GITHUB_TOKEN: undefined,
|
|
787
|
+
PATH: `${binDir};${process.env.PATH ?? ""}`,
|
|
788
|
+
PATHEXT: ".COM;.EXE;.BAT;.CMD",
|
|
789
|
+
}, () => {
|
|
790
|
+
try {
|
|
791
|
+
const results = runProviderChecks();
|
|
792
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
793
|
+
assert.ok(anthropic, "anthropic result should exist");
|
|
794
|
+
assert.equal(anthropic!.status, "ok", "should be ok when claude.exe is in PATH (#4548)");
|
|
795
|
+
assert.ok(anthropic!.message.toLowerCase().includes("claude"), "should mention claude-code as source");
|
|
796
|
+
} finally {
|
|
797
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
771
802
|
test("PROVIDER_ROUTES includes google-gemini-cli as route for google (#2922)", async () => {
|
|
772
803
|
const { readFileSync: readFS } = await import("node:fs");
|
|
773
804
|
const { dirname: dirn, join: joinPath } = await import("node:path");
|
|
@@ -389,7 +389,7 @@ describe('ensure-db-open', () => {
|
|
|
389
389
|
assert.ok(db, 'adapter should be available after ensureDbOpen');
|
|
390
390
|
assert.equal(
|
|
391
391
|
db.prepare('SELECT MAX(version) as version FROM schema_version').get()?.version,
|
|
392
|
-
|
|
392
|
+
22,
|
|
393
393
|
'legacy DB should migrate to current schema version',
|
|
394
394
|
);
|
|
395
395
|
|
|
@@ -348,7 +348,7 @@ test("ADR-011 P2: schema v20 fresh DB has all escalation columns on tasks + sour
|
|
|
348
348
|
assert.ok(decCols.includes("source"), "decisions table must have source column");
|
|
349
349
|
|
|
350
350
|
const version = adapter.prepare("SELECT MAX(version) as v FROM schema_version").get();
|
|
351
|
-
assert.equal(version?.["v"],
|
|
351
|
+
assert.equal(version?.["v"], 22);
|
|
352
352
|
});
|
|
353
353
|
|
|
354
354
|
test("ADR-011 P2: findUnappliedEscalationOverride returns null when escalation_pending=1 (still pending)", (t) => {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { listExecHistory, searchExecHistory } from '../exec-history.ts';
|
|
8
|
+
import { executeExecSearch } from '../tools/exec-search-tool.ts';
|
|
9
|
+
|
|
10
|
+
function freshBase(): string {
|
|
11
|
+
return mkdtempSync(join(tmpdir(), 'gsd-exec-history-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cleanup(dir: string): void {
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeRun(base: string, id: string, overrides: Record<string, unknown> = {}): void {
|
|
19
|
+
const dir = join(base, '.gsd', 'exec');
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
const stdoutPath = join(dir, `${id}.stdout`);
|
|
22
|
+
const stderrPath = join(dir, `${id}.stderr`);
|
|
23
|
+
const metaPath = join(dir, `${id}.meta.json`);
|
|
24
|
+
writeFileSync(stdoutPath, (overrides.stdout as string | undefined) ?? `stdout for ${id}\n`);
|
|
25
|
+
writeFileSync(stderrPath, '');
|
|
26
|
+
writeFileSync(
|
|
27
|
+
metaPath,
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
id,
|
|
30
|
+
runtime: 'bash',
|
|
31
|
+
purpose: `purpose for ${id}`,
|
|
32
|
+
started_at: '2026-04-20T12:00:00.000Z',
|
|
33
|
+
finished_at: '2026-04-20T12:00:00.100Z',
|
|
34
|
+
duration_ms: 100,
|
|
35
|
+
exit_code: 0,
|
|
36
|
+
signal: null,
|
|
37
|
+
timed_out: false,
|
|
38
|
+
stdout_bytes: 12,
|
|
39
|
+
stderr_bytes: 0,
|
|
40
|
+
stdout_truncated: false,
|
|
41
|
+
stderr_truncated: false,
|
|
42
|
+
stdout_path: stdoutPath,
|
|
43
|
+
stderr_path: stderrPath,
|
|
44
|
+
...overrides,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('listExecHistory: returns empty list when .gsd/exec missing', () => {
|
|
50
|
+
const base = freshBase();
|
|
51
|
+
try {
|
|
52
|
+
assert.deepEqual(listExecHistory(base), []);
|
|
53
|
+
} finally {
|
|
54
|
+
cleanup(base);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('listExecHistory: skips malformed meta files', () => {
|
|
59
|
+
const base = freshBase();
|
|
60
|
+
try {
|
|
61
|
+
const dir = join(base, '.gsd', 'exec');
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
writeFileSync(join(dir, 'bad.meta.json'), '{not-json');
|
|
64
|
+
writeRun(base, 'ok-1');
|
|
65
|
+
const list = listExecHistory(base);
|
|
66
|
+
assert.equal(list.length, 1);
|
|
67
|
+
assert.equal(list[0]!.id, 'ok-1');
|
|
68
|
+
} finally {
|
|
69
|
+
cleanup(base);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('searchExecHistory: filters by query, runtime, and failing_only', () => {
|
|
74
|
+
const base = freshBase();
|
|
75
|
+
try {
|
|
76
|
+
writeRun(base, 'playwright-run', { purpose: 'playwright snapshot' });
|
|
77
|
+
writeRun(base, 'grep-run', { purpose: 'grep TODOs' });
|
|
78
|
+
writeRun(base, 'failing-run', { exit_code: 1, purpose: 'boom' });
|
|
79
|
+
writeRun(base, 'node-run', { runtime: 'node', purpose: 'dedupe' });
|
|
80
|
+
|
|
81
|
+
const playwrightHits = searchExecHistory(base, { query: 'playwright' });
|
|
82
|
+
assert.equal(playwrightHits.length, 1);
|
|
83
|
+
assert.equal(playwrightHits[0]!.entry.id, 'playwright-run');
|
|
84
|
+
|
|
85
|
+
const failingHits = searchExecHistory(base, { failing_only: true });
|
|
86
|
+
assert.equal(failingHits.length, 1);
|
|
87
|
+
assert.equal(failingHits[0]!.entry.id, 'failing-run');
|
|
88
|
+
|
|
89
|
+
const nodeHits = searchExecHistory(base, { runtime: 'node' });
|
|
90
|
+
assert.equal(nodeHits.length, 1);
|
|
91
|
+
assert.equal(nodeHits[0]!.entry.runtime, 'node');
|
|
92
|
+
|
|
93
|
+
const unlimited = searchExecHistory(base, {});
|
|
94
|
+
assert.equal(unlimited.length, 4);
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(base);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('executeExecSearch: returns helpful empty-state message when no matches', () => {
|
|
101
|
+
const base = freshBase();
|
|
102
|
+
try {
|
|
103
|
+
const result = executeExecSearch({ query: 'missing' }, { baseDir: base });
|
|
104
|
+
assert.ok(!result.isError);
|
|
105
|
+
assert.match(result.content[0].text, /No prior gsd_exec runs/);
|
|
106
|
+
} finally {
|
|
107
|
+
cleanup(base);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('executeExecSearch: includes stdout_path and preview in details', () => {
|
|
112
|
+
const base = freshBase();
|
|
113
|
+
try {
|
|
114
|
+
writeRun(base, 'summary-run', { stdout: 'found 42 TODOs\n' });
|
|
115
|
+
const result = executeExecSearch({ query: 'summary' }, { baseDir: base });
|
|
116
|
+
const details = result.details as { results: Array<{ id: string; stdout_path: string }> };
|
|
117
|
+
assert.equal(details.results.length, 1);
|
|
118
|
+
assert.equal(details.results[0]!.id, 'summary-run');
|
|
119
|
+
assert.match(details.results[0]!.stdout_path, /summary-run\.stdout$/);
|
|
120
|
+
assert.match(result.content[0].text, /found 42 TODOs/);
|
|
121
|
+
} finally {
|
|
122
|
+
cleanup(base);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
|
|
8
|
+
import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
|
|
9
|
+
import { isContextModeEnabled } from '../preferences-types.ts';
|
|
10
|
+
|
|
11
|
+
function freshBase(): string {
|
|
12
|
+
return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanup(dir: string): void {
|
|
16
|
+
rmSync(dir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function baseOpts(base: string, overrides: Partial<ExecSandboxOptions> = {}): ExecSandboxOptions {
|
|
20
|
+
return {
|
|
21
|
+
baseDir: base,
|
|
22
|
+
clamp_timeout_ms: EXEC_DEFAULTS.clampTimeoutMs,
|
|
23
|
+
default_timeout_ms: 10_000,
|
|
24
|
+
stdout_cap_bytes: 1_024,
|
|
25
|
+
stderr_cap_bytes: 1_024,
|
|
26
|
+
digest_chars: 120,
|
|
27
|
+
env_allowlist: EXEC_DEFAULTS.envAllowlist,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('runExecSandbox: captures stdout, persists artifacts, returns digest', async () => {
|
|
33
|
+
const base = freshBase();
|
|
34
|
+
try {
|
|
35
|
+
const result = await runExecSandbox(
|
|
36
|
+
{ runtime: 'bash', script: 'echo hello world' },
|
|
37
|
+
baseOpts(base),
|
|
38
|
+
);
|
|
39
|
+
assert.equal(result.exit_code, 0);
|
|
40
|
+
assert.equal(result.timed_out, false);
|
|
41
|
+
assert.ok(result.digest.includes('hello world'), `digest should contain stdout: ${result.digest}`);
|
|
42
|
+
assert.ok(result.stdout_path.startsWith(join(base, '.gsd', 'exec')), 'stdout path under .gsd/exec');
|
|
43
|
+
assert.equal(readFileSync(result.stdout_path, 'utf-8').trim(), 'hello world');
|
|
44
|
+
const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8')) as Record<string, unknown>;
|
|
45
|
+
assert.equal(meta.runtime, 'bash');
|
|
46
|
+
assert.equal(meta.exit_code, 0);
|
|
47
|
+
} finally {
|
|
48
|
+
cleanup(base);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('runExecSandbox: enforces stdout cap and marks truncation', async () => {
|
|
53
|
+
const base = freshBase();
|
|
54
|
+
try {
|
|
55
|
+
const result = await runExecSandbox(
|
|
56
|
+
// Emit far more than the cap so truncation triggers.
|
|
57
|
+
{ runtime: 'bash', script: 'head -c 8000 /dev/urandom | base64' },
|
|
58
|
+
baseOpts(base, { stdout_cap_bytes: 256 }),
|
|
59
|
+
);
|
|
60
|
+
assert.equal(result.stdout_truncated, true, 'should mark stdout truncated');
|
|
61
|
+
assert.ok(result.stdout_bytes <= 256, `stdout_bytes within cap (got ${result.stdout_bytes})`);
|
|
62
|
+
const stdout = readFileSync(result.stdout_path, 'utf-8');
|
|
63
|
+
assert.ok(stdout.endsWith('[truncated: stdout cap reached]\n'), 'truncation marker appended');
|
|
64
|
+
} finally {
|
|
65
|
+
cleanup(base);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('runExecSandbox: enforces timeout and surfaces timed_out', async () => {
|
|
70
|
+
const base = freshBase();
|
|
71
|
+
try {
|
|
72
|
+
const started = Date.now();
|
|
73
|
+
const result = await runExecSandbox(
|
|
74
|
+
{ runtime: 'bash', script: 'sleep 10' },
|
|
75
|
+
baseOpts(base, { default_timeout_ms: 150, clamp_timeout_ms: 150 }),
|
|
76
|
+
);
|
|
77
|
+
const elapsed = Date.now() - started;
|
|
78
|
+
assert.equal(result.timed_out, true);
|
|
79
|
+
assert.ok(elapsed < 5_000, `should return well before 10s (took ${elapsed}ms)`);
|
|
80
|
+
} finally {
|
|
81
|
+
cleanup(base);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('runExecSandbox: forwards only allowlisted env vars', async () => {
|
|
86
|
+
const base = freshBase();
|
|
87
|
+
try {
|
|
88
|
+
const result = await runExecSandbox(
|
|
89
|
+
{ runtime: 'bash', script: 'echo PATH=$PATH SECRET=$GSD_TEST_SECRET' },
|
|
90
|
+
baseOpts(base, {
|
|
91
|
+
env_allowlist: [],
|
|
92
|
+
env: { PATH: '/usr/bin:/bin', HOME: '/tmp', GSD_TEST_SECRET: 'should-be-blocked' },
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
const stdout = readFileSync(result.stdout_path, 'utf-8');
|
|
96
|
+
assert.ok(stdout.includes('PATH=/usr/bin:/bin'), 'PATH forwarded');
|
|
97
|
+
assert.ok(!stdout.includes('should-be-blocked'), 'non-allowlisted var blocked');
|
|
98
|
+
} finally {
|
|
99
|
+
cleanup(base);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('runExecSandbox: node runtime executes JS', async () => {
|
|
104
|
+
const base = freshBase();
|
|
105
|
+
try {
|
|
106
|
+
const result = await runExecSandbox(
|
|
107
|
+
{ runtime: 'node', script: 'console.log("node-ok:" + (1+2))' },
|
|
108
|
+
baseOpts(base),
|
|
109
|
+
);
|
|
110
|
+
assert.equal(result.exit_code, 0);
|
|
111
|
+
assert.ok(result.digest.includes('node-ok:3'));
|
|
112
|
+
} finally {
|
|
113
|
+
cleanup(base);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── exec-tool executor ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
test('executeGsdExec: runs by default when context_mode is unset', async () => {
|
|
120
|
+
const base = freshBase();
|
|
121
|
+
try {
|
|
122
|
+
const result = await executeGsdExec(
|
|
123
|
+
{ runtime: 'bash', script: 'echo default-on-run' },
|
|
124
|
+
{ baseDir: base, preferences: {} },
|
|
125
|
+
);
|
|
126
|
+
assert.ok(!result.isError, 'should succeed with no preferences');
|
|
127
|
+
assert.equal(result.details.operation, 'gsd_exec');
|
|
128
|
+
assert.equal(result.details.exit_code, 0);
|
|
129
|
+
assert.ok(result.content[0].text.includes('default-on-run'));
|
|
130
|
+
} finally {
|
|
131
|
+
cleanup(base);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('executeGsdExec: runs when preferences is null (fresh project)', async () => {
|
|
136
|
+
const base = freshBase();
|
|
137
|
+
try {
|
|
138
|
+
const result = await executeGsdExec(
|
|
139
|
+
{ runtime: 'bash', script: 'echo null-prefs-run' },
|
|
140
|
+
{ baseDir: base, preferences: null },
|
|
141
|
+
);
|
|
142
|
+
assert.ok(!result.isError, 'null preferences should not disable');
|
|
143
|
+
assert.ok(result.content[0].text.includes('null-prefs-run'));
|
|
144
|
+
} finally {
|
|
145
|
+
cleanup(base);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('executeGsdExec: blocked only when context_mode.enabled=false', async () => {
|
|
150
|
+
const base = freshBase();
|
|
151
|
+
try {
|
|
152
|
+
const result = await executeGsdExec(
|
|
153
|
+
{ runtime: 'bash', script: 'echo should-not-run' },
|
|
154
|
+
{ baseDir: base, preferences: { context_mode: { enabled: false } } },
|
|
155
|
+
);
|
|
156
|
+
assert.equal(result.isError, true);
|
|
157
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup(base);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('executeGsdExec: runs when enabled explicitly set to true', async () => {
|
|
164
|
+
const base = freshBase();
|
|
165
|
+
try {
|
|
166
|
+
const result = await executeGsdExec(
|
|
167
|
+
{ runtime: 'bash', script: 'echo explicit-on' },
|
|
168
|
+
{ baseDir: base, preferences: { context_mode: { enabled: true } } },
|
|
169
|
+
);
|
|
170
|
+
assert.ok(!result.isError);
|
|
171
|
+
assert.ok(result.content[0].text.includes('explicit-on'));
|
|
172
|
+
} finally {
|
|
173
|
+
cleanup(base);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('executeGsdExec: rejects empty script', async () => {
|
|
178
|
+
const base = freshBase();
|
|
179
|
+
try {
|
|
180
|
+
const result = await executeGsdExec(
|
|
181
|
+
{ runtime: 'bash', script: ' ' },
|
|
182
|
+
{ baseDir: base, preferences: { context_mode: { enabled: true } } },
|
|
183
|
+
);
|
|
184
|
+
assert.equal(result.isError, true);
|
|
185
|
+
assert.equal((result.details as { error?: string }).error, 'invalid_params');
|
|
186
|
+
} finally {
|
|
187
|
+
cleanup(base);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
|
|
192
|
+
assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
|
|
193
|
+
assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
|
|
194
|
+
assert.equal(isContextModeEnabled({}), true, 'empty prefs → on');
|
|
195
|
+
assert.equal(isContextModeEnabled({ context_mode: {} }), true, 'empty block → on');
|
|
196
|
+
assert.equal(isContextModeEnabled({ context_mode: { enabled: true } }), true);
|
|
197
|
+
assert.equal(isContextModeEnabled({ context_mode: { enabled: false } }), false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('buildExecOptions: clamps out-of-range values to safe defaults', () => {
|
|
201
|
+
const opts = buildExecOptions('/tmp/base', {
|
|
202
|
+
enabled: true,
|
|
203
|
+
exec_timeout_ms: 999_999_999,
|
|
204
|
+
exec_stdout_cap_bytes: 1,
|
|
205
|
+
exec_digest_chars: -20,
|
|
206
|
+
});
|
|
207
|
+
assert.equal(opts.default_timeout_ms, EXEC_DEFAULTS.clampTimeoutMs, 'timeout clamped to upper bound');
|
|
208
|
+
assert.equal(opts.stdout_cap_bytes, 4_096, 'stdout cap clamped to floor');
|
|
209
|
+
assert.equal(opts.digest_chars, 0, 'digest chars clamped to floor');
|
|
210
|
+
});
|
|
@@ -35,6 +35,44 @@ test("validateFileChanges works on repos with a single commit (no HEAD~1)", (t)
|
|
|
35
35
|
assert.deepEqual(audit.missingFiles, []);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test("validateFileChanges excludes allowlisted files from unexpected-change warnings", (t) => {
|
|
39
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
40
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
41
|
+
|
|
42
|
+
mkdirSync(join(base, "tracking", "history"), { recursive: true });
|
|
43
|
+
git(base, "init");
|
|
44
|
+
git(base, "config", "user.email", "test@example.com");
|
|
45
|
+
git(base, "config", "user.name", "Test User");
|
|
46
|
+
|
|
47
|
+
writeFileSync(join(base, "src.ts"), "initial\n");
|
|
48
|
+
writeFileSync(join(base, "tracking", "history", "2026-04-20-snapshot.md"), "initial\n");
|
|
49
|
+
git(base, "add", ".");
|
|
50
|
+
git(base, "commit", "-m", "initial");
|
|
51
|
+
|
|
52
|
+
writeFileSync(join(base, "src.ts"), "updated\n");
|
|
53
|
+
writeFileSync(join(base, "tracking", "history", "2026-04-20-snapshot.md"), "updated\n");
|
|
54
|
+
git(base, "add", ".");
|
|
55
|
+
git(base, "commit", "-m", "update");
|
|
56
|
+
|
|
57
|
+
// Without allowlist: tracking/history snapshot is unexpected
|
|
58
|
+
const auditWithout = validateFileChanges(base, ["src.ts"], []);
|
|
59
|
+
assert.ok(auditWithout, "audit should be produced");
|
|
60
|
+
assert.ok(
|
|
61
|
+
auditWithout.unexpectedFiles.includes("tracking/history/2026-04-20-snapshot.md"),
|
|
62
|
+
"snapshot should be unexpected without allowlist",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// With glob allowlist: snapshot is excluded
|
|
66
|
+
const auditWith = validateFileChanges(base, ["src.ts"], [], ["tracking/history/**"]);
|
|
67
|
+
assert.ok(auditWith, "audit should be produced with allowlist");
|
|
68
|
+
assert.deepEqual(auditWith.unexpectedFiles, [], "no unexpected files when snapshot is allowlisted");
|
|
69
|
+
assert.equal(
|
|
70
|
+
auditWith.violations.filter(v => v.severity === "warning").length,
|
|
71
|
+
0,
|
|
72
|
+
"no warnings when all unexpected files are allowlisted",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
38
76
|
test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
|
|
39
77
|
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
40
78
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|