create-claude-cabinet 0.47.0 → 0.48.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.
Files changed (76) hide show
  1. package/README.md +1 -0
  2. package/lib/CLAUDE.md +218 -0
  3. package/lib/cli.js +102 -344
  4. package/lib/copy.js +75 -1
  5. package/lib/engagement-server-setup.js +1 -17
  6. package/lib/engagement-setup.js +1 -1
  7. package/lib/installer-gate.js +135 -0
  8. package/lib/modules.js +292 -0
  9. package/lib/mux-setup.js +1 -17
  10. package/lib/project-context.js +16 -96
  11. package/lib/settings-merge.js +148 -9
  12. package/lib/site-audit-setup.js +23 -7
  13. package/lib/verify-setup.js +20 -9
  14. package/lib/watchtower-setup.js +1 -5
  15. package/package.json +1 -1
  16. package/templates/CLAUDE.md +959 -0
  17. package/templates/briefing/_briefing-template.md +6 -11
  18. package/templates/cabinet/eval-protocol.md +27 -10
  19. package/templates/cabinet/watchtower-contracts.md +39 -0
  20. package/templates/cabinet/worktree-invocation-contract.md +24 -0
  21. package/templates/engagement/__tests__/checklist-visibility.test.mjs +76 -0
  22. package/templates/engagement/engagement-checklist.mjs +26 -2
  23. package/templates/engagement/engagement-schema.md +8 -6
  24. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +49 -9
  25. package/templates/engagement/pib-db-patches/pib-db-schema.sql +1 -1
  26. package/templates/engagement/pib-db-patches/pib-db.mjs +4 -1
  27. package/templates/engagement/sql-constants.mjs +9 -1
  28. package/templates/engagement-server/__tests__/cross-tenant-auth.test.mjs +87 -0
  29. package/templates/engagement-server/server.mjs +65 -40
  30. package/templates/hooks/action-completion-gate.sh +10 -0
  31. package/templates/hooks/action-quality-gate.sh +10 -0
  32. package/templates/hooks/skill-telemetry.sh +10 -5
  33. package/templates/hooks/skill-tool-telemetry.sh +11 -5
  34. package/templates/mcp/pib-db.json +1 -1
  35. package/templates/rules/enforcement-pipeline.md +0 -12
  36. package/templates/scripts/__tests__/api-usage-idle-gate.test.mjs +81 -0
  37. package/templates/scripts/__tests__/bsql-loader-verify.test.mjs +47 -0
  38. package/templates/scripts/__tests__/inbox-assessment.test.mjs +341 -0
  39. package/templates/scripts/__tests__/memory-reachability.test.mjs +150 -0
  40. package/templates/scripts/__tests__/ring1-pib-error-surfacing.test.mjs +69 -0
  41. package/templates/scripts/__tests__/ring1-stale-open-counts.test.mjs +143 -0
  42. package/templates/scripts/__tests__/ring3-thread-capture.test.mjs +213 -0
  43. package/templates/scripts/__tests__/watchtower-status-ring4.test.mjs +89 -0
  44. package/templates/scripts/audit-coherence-check.mjs +123 -0
  45. package/templates/scripts/audit-synth.mjs +117 -0
  46. package/templates/scripts/merge-findings.js +4 -1
  47. package/templates/scripts/patterns-scope-check.mjs +122 -0
  48. package/templates/scripts/pib-db-lib.mjs +61 -11
  49. package/templates/scripts/pib-db-mcp-server.mjs +2 -1
  50. package/templates/scripts/pib-db.mjs +2 -1
  51. package/templates/scripts/review-server.mjs +7 -12
  52. package/templates/scripts/triage-server.mjs +7 -8
  53. package/templates/scripts/watchtower-cross-ring-reader.mjs +20 -6
  54. package/templates/scripts/watchtower-inbox-assessment.mjs +706 -0
  55. package/templates/scripts/watchtower-lib.mjs +282 -4
  56. package/templates/scripts/watchtower-ring1.mjs +78 -38
  57. package/templates/scripts/watchtower-ring2.mjs +38 -12
  58. package/templates/scripts/watchtower-ring3-close.mjs +160 -21
  59. package/templates/scripts/watchtower-status.sh +17 -0
  60. package/templates/scripts/work-tracker-server.mjs +26 -22
  61. package/templates/skills/audit/SKILL.md +18 -5
  62. package/templates/skills/audit/phases/structural-checks.md +22 -0
  63. package/templates/skills/briefing/SKILL.md +8 -0
  64. package/templates/skills/cabinet-accessibility/SKILL.md +1 -1
  65. package/templates/skills/cc-publish/SKILL.md +41 -7
  66. package/templates/skills/debrief/phases/audit-pattern-capture.md +13 -0
  67. package/templates/skills/inbox/SKILL.md +95 -5
  68. package/templates/skills/orient/SKILL.md +4 -0
  69. package/templates/skills/pulse/SKILL.md +10 -8
  70. package/templates/skills/spring-clean/SKILL.md +1 -1
  71. package/templates/skills/spring-clean/phases/execute-decisions.md +1 -1
  72. package/templates/skills/validate/phases/validators.md +86 -0
  73. package/templates/skills/watchtower/SKILL.md +18 -26
  74. package/templates/workflows/deliberative-audit.js +258 -87
  75. package/templates/workflows/execute-group-complete.js +16 -2
  76. package/templates/workflows/execute-group-implement.js +16 -2
@@ -102,6 +102,64 @@ function resolveExternalUser(engagementId, email, roleMapping) {
102
102
  return null;
103
103
  }
104
104
 
105
+ // SECURITY (security-0003): pick EXACTLY ONE external engagement to forward a
106
+ // bearer token to — NEVER fan out across tenants. Scope by the X-Engagement-Id
107
+ // header; if it is absent, only a single-external-engagement deployment is
108
+ // unambiguous. A header naming an unknown/non-external engagement gets no
109
+ // fallback (returns null). Fanning the raw token out to every external
110
+ // engagement's validate_url leaked tenant A's credential to tenant B's infra
111
+ // and let an anonymous caller trigger N outbound HTTPS fetches.
112
+ function selectExternalTarget(externals, requestedId) {
113
+ if (requestedId) return externals.find((e) => e.id === requestedId) || null;
114
+ if (externals.length === 1) return externals[0];
115
+ return null;
116
+ }
117
+
118
+ // Forward the token to ONE engagement's validate_url and resolve the user.
119
+ // Returns the authenticated user object or null (never throws).
120
+ async function validateTokenAgainstEngagement(eng, raw) {
121
+ try {
122
+ const config = JSON.parse(eng.auth_config);
123
+ if (!config.validate_url) return null;
124
+
125
+ const resp = await fetch(config.validate_url, {
126
+ headers: { 'Authorization': `Bearer ${raw}` },
127
+ signal: AbortSignal.timeout(5000),
128
+ });
129
+ if (!resp.ok) return null;
130
+
131
+ const identity = await resp.json();
132
+ const email = identity.email;
133
+ if (!email) return null;
134
+
135
+ // Map platform role to engagement role via role_mapping
136
+ const roleMapping = config.role_mapping || {};
137
+ const mappedRole = roleMapping[identity.role] || identity.role;
138
+
139
+ // Find matching local user by email for message attribution
140
+ const user = resolveExternalUser(eng.id, email, roleMapping);
141
+ if (user) return { ...user, auth_mode: 'external' };
142
+
143
+ // User authenticated with platform but no local user record —
144
+ // auto-create so messages are properly attributed
145
+ const userId = `usr_${randomBytes(6).toString('hex')}`;
146
+ const engRole = ['consultant', 'client'].includes(mappedRole) ? mappedRole : 'client';
147
+ db.prepare(`INSERT INTO users (id, engagement_id, name, email, role) VALUES (?, ?, ?, ?, ?)`)
148
+ .run(userId, eng.id, email.split('@')[0], email, engRole);
149
+
150
+ return {
151
+ user_id: userId,
152
+ engagement_id: eng.id,
153
+ role: engRole,
154
+ user_name: email.split('@')[0],
155
+ auth_mode: 'external',
156
+ };
157
+ } catch (err) {
158
+ console.error(`External auth failed for engagement ${eng.id}: ${err.message}`);
159
+ return null;
160
+ }
161
+ }
162
+
105
163
  async function authenticateRequest(req) {
106
164
  const authHeader = req.headers['authorization'];
107
165
  if (!authHeader || !authHeader.startsWith('Bearer ')) return null;
@@ -112,47 +170,14 @@ async function authenticateRequest(req) {
112
170
  const localData = resolveLocalToken(hashToken(raw));
113
171
  if (localData && localData.auth_mode === 'local') return localData;
114
172
 
115
- // Path 2: external auth — forward token to client app's validate_url
173
+ // Path 2: external auth — forward the token to a SINGLE client app's
174
+ // validate_url (never a cross-tenant fan-out; see selectExternalTarget).
116
175
  const externals = getExternalEngagements();
117
- for (const eng of externals) {
118
- try {
119
- const config = JSON.parse(eng.auth_config);
120
- if (!config.validate_url) continue;
121
-
122
- const resp = await fetch(config.validate_url, {
123
- headers: { 'Authorization': `Bearer ${raw}` },
124
- signal: AbortSignal.timeout(5000),
125
- });
126
- if (!resp.ok) continue;
127
-
128
- const identity = await resp.json();
129
- const email = identity.email;
130
- if (!email) continue;
131
-
132
- // Map platform role to engagement role via role_mapping
133
- const roleMapping = config.role_mapping || {};
134
- const mappedRole = roleMapping[identity.role] || identity.role;
135
-
136
- // Find matching local user by email for message attribution
137
- const user = resolveExternalUser(eng.id, email, roleMapping);
138
- if (user) return { ...user, auth_mode: 'external' };
139
-
140
- // User authenticated with platform but no local user record —
141
- // auto-create so messages are properly attributed
142
- const userId = `usr_${randomBytes(6).toString('hex')}`;
143
- const engRole = ['consultant', 'client'].includes(mappedRole) ? mappedRole : 'client';
144
- db.prepare(`INSERT INTO users (id, engagement_id, name, email, role) VALUES (?, ?, ?, ?, ?)`)
145
- .run(userId, eng.id, email.split('@')[0], email, engRole);
146
-
147
- return {
148
- user_id: userId,
149
- engagement_id: eng.id,
150
- role: engRole,
151
- user_name: email.split('@')[0],
152
- auth_mode: 'external',
153
- };
154
- } catch (err) {
155
- console.error(`External auth failed for engagement ${eng.id}: ${err.message}`);
176
+ if (externals.length > 0) {
177
+ const target = selectExternalTarget(externals, req.headers['x-engagement-id']);
178
+ if (target) {
179
+ const authed = await validateTokenAgainstEngagement(target, raw);
180
+ if (authed) return authed;
156
181
  }
157
182
  }
158
183
 
@@ -14,6 +14,16 @@
14
14
  # (the pib_complete_action args) is under `tool_input` (fall back to
15
15
  # top-level for older payload shapes).
16
16
  INPUT=$(cat)
17
+
18
+ # Fired-at-least-once telemetry (act:ff693d4c). Record that this gate's matcher
19
+ # actually matched and the hook ran — a dead matcher (the bare
20
+ # `pib_complete_action` spelling that never matched the real
21
+ # `mcp__pib-db__pib_complete_action` MCP tool name) leaves this file empty
22
+ # forever. Env-overridable for tests; fail-open so telemetry never breaks the gate.
23
+ FIRED_LOG="${CC_HOOK_FIRED_LOG:-${CLAUDE_PROJECT_DIR:-.}/.claude/state/hooks-fired.jsonl}"
24
+ mkdir -p "$(dirname "$FIRED_LOG")" 2>/dev/null \
25
+ && printf '{"hook":"action-completion-gate","fired_at":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$FIRED_LOG" 2>/dev/null || true
26
+
17
27
  FID=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); ti=d.get('tool_input', d); print(ti.get('fid',''))" 2>/dev/null)
18
28
 
19
29
  if [ -z "$FID" ]; then
@@ -17,6 +17,16 @@
17
17
  # top-level for older payload shapes). stdin is read ONCE — reuse $INPUT.
18
18
  INPUT=$(cat)
19
19
 
20
+ # Fired-at-least-once telemetry (act:ff693d4c). Record that this gate's matcher
21
+ # actually matched and the hook ran. A dead matcher (the bare `pib_create_action`
22
+ # spelling that never matched the real `mcp__pib-db__pib_create_action` MCP tool
23
+ # name) leaves this file empty forever — which is what let this gate stay
24
+ # silently dead since it shipped. Env-overridable for tests; fail-open so
25
+ # telemetry can never break the gate.
26
+ FIRED_LOG="${CC_HOOK_FIRED_LOG:-${CLAUDE_PROJECT_DIR:-.}/.claude/state/hooks-fired.jsonl}"
27
+ mkdir -p "$(dirname "$FIRED_LOG")" 2>/dev/null \
28
+ && printf '{"hook":"action-quality-gate","fired_at":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$FIRED_LOG" 2>/dev/null || true
29
+
20
30
  NOTES=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); ti=d.get('tool_input', d); print(ti.get('notes',''))" 2>/dev/null)
21
31
  TEXT=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); ti=d.get('tool_input', d); print(ti.get('text',''))" 2>/dev/null)
22
32
 
@@ -46,13 +46,18 @@ if [[ "$PROMPT" =~ ^/([a-z-]+) ]]; then
46
46
  for skill in $SKILLS; do
47
47
  if [[ "$COMMAND" == "$skill" ]]; then
48
48
  TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
49
- python3 -c "
50
- import json
49
+ # SECURITY (security-0004): env-pass values; never interpolate hook
50
+ # payload fields into the python3 -c source (same class as
51
+ # skill-tool-telemetry.sh). COMMAND is regex-constrained to [a-z-]+
52
+ # here, but session_id is passed through raw — os.environ keeps both
53
+ # inert.
54
+ SKILL="$COMMAND" SESSION_ID="$SESSION_ID" TIMESTAMP="$TIMESTAMP" python3 -c "
55
+ import json, os
51
56
  record = {
52
- 'ts': '$TIMESTAMP',
57
+ 'ts': os.environ['TIMESTAMP'],
53
58
  'event': 'skill-invoke',
54
- 'skill': '$COMMAND',
55
- 'session_id': '$SESSION_ID'
59
+ 'skill': os.environ['SKILL'],
60
+ 'session_id': os.environ['SESSION_ID'],
56
61
  }
57
62
  print(json.dumps(record))
58
63
  " >> "$TELEMETRY_FILE"
@@ -37,14 +37,20 @@ except:
37
37
 
38
38
  if [[ -n "$SKILL" ]]; then
39
39
  TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
40
- python3 -c "
41
- import json
40
+ # SECURITY (security-0004): pass values via ENV, never concatenate hook
41
+ # payload fields into the python3 -c source. Interpolating '$SKILL' into the
42
+ # program let a skill value with a quote/newline (model-composed from
43
+ # tool_input.skill, reachable via prompt injection) break out of the string
44
+ # literal and execute arbitrary Python — command execution in a PostToolUse
45
+ # hook. os.environ reads them as inert data.
46
+ SKILL="$SKILL" SESSION_ID="$SESSION_ID" TIMESTAMP="$TIMESTAMP" python3 -c "
47
+ import json, os
42
48
  record = {
43
- 'ts': '$TIMESTAMP',
49
+ 'ts': os.environ['TIMESTAMP'],
44
50
  'event': 'skill-invoke',
45
- 'skill': '$SKILL',
51
+ 'skill': os.environ['SKILL'],
46
52
  'source': 'tool',
47
- 'session_id': '$SESSION_ID'
53
+ 'session_id': os.environ['SESSION_ID'],
48
54
  }
49
55
  print(json.dumps(record))
50
56
  " >> "$TELEMETRY_FILE"
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pib-db": {
4
4
  "command": "node",
5
- "args": ["scripts/pib-db-mcp-server.mjs"]
5
+ "args": ["${CLAUDE_PROJECT_DIR:-.}/scripts/pib-db-mcp-server.mjs"]
6
6
  }
7
7
  }
8
8
  }
@@ -141,15 +141,3 @@ The audit skill's triage history tracks which findings recur. The
141
141
  enforcement pipeline's pattern files track which recurring findings are
142
142
  ready for promotion. Together, they create a feedback loop from
143
143
  observation to prevention.
144
-
145
- ## Current State
146
-
147
- <!--
148
- Track your project's enforcement pipeline state here:
149
-
150
- - **N pattern files** in memory/patterns/
151
- - **N raw observations** in memory/archive/ (or wherever you store them)
152
- - **N rules files** in .claude/rules/
153
- - **N hooks** in .claude/settings.json
154
- - **N promotion candidates** identified
155
- -->
@@ -0,0 +1,81 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import {
7
+ recordApiUsage, readApiUsageRecords, summarizeApiUsage, API_PRICING,
8
+ computeActivitySignature, shouldRunIdlePass, markIdlePassRan, RING2_IDLE_FLOOR_MS,
9
+ } from '../watchtower-lib.mjs';
10
+
11
+ function tmpWt() { return mkdtempSync(join(tmpdir(), 'wt-usage-')); }
12
+
13
+ test('summarizeApiUsage: totals + $ estimate + window filter', () => {
14
+ const now = new Date('2026-07-12T12:00:00Z').getTime();
15
+ const recs = [
16
+ { ts: '2026-07-12T11:00:00Z', ring: 'ring3', model: 'claude-sonnet-4-6', input_tokens: 1_000_000, output_tokens: 100_000 },
17
+ { ts: '2026-07-12T11:30:00Z', ring: 'ring2', model: 'claude-sonnet-4-6', input_tokens: 500_000, output_tokens: 10_000 },
18
+ { ts: '2026-07-01T00:00:00Z', ring: 'ring2', model: 'claude-sonnet-4-6', input_tokens: 9_999_999, output_tokens: 9_999_999 }, // outside 1d window
19
+ ];
20
+ const s = summarizeApiUsage(recs, { nowMs: now, windowDays: 1 });
21
+ assert.equal(s.calls, 2, 'old record excluded by window');
22
+ assert.equal(s.input_tokens, 1_500_000);
23
+ assert.equal(s.output_tokens, 110_000);
24
+ // ring3: 1M*$3 + 100k*$15 = $3 + $1.5 = $4.50 ; ring2: 500k*$3 + 10k*$15 = $1.5 + $0.15 = $1.65
25
+ assert.equal(s.est_cost_usd, 6.15);
26
+ assert.equal(s.by_ring.ring3, 4.5);
27
+ assert.equal(s.by_ring.ring2, 1.65);
28
+ });
29
+
30
+ test('summarizeApiUsage: empty + unknown model falls back to sonnet pricing', () => {
31
+ assert.equal(summarizeApiUsage([]).calls, 0);
32
+ const s = summarizeApiUsage([{ ts: new Date().toISOString(), model: 'mystery', input_tokens: 1_000_000, output_tokens: 0 }], { windowDays: 3650 });
33
+ assert.equal(s.est_cost_usd, 3, 'unknown model priced as sonnet-4-6 ($3/M in)');
34
+ });
35
+
36
+ test('recordApiUsage → readApiUsageRecords round-trips, fail-open on bad dir', () => {
37
+ const wt = tmpWt();
38
+ try {
39
+ recordApiUsage({ ring: 'ring3', model: 'claude-sonnet-4-6', usage: { input_tokens: 42, output_tokens: 7 } }, { watchtowerDir: wt });
40
+ const recs = readApiUsageRecords(wt);
41
+ assert.equal(recs.length, 1);
42
+ assert.equal(recs[0].input_tokens, 42);
43
+ assert.equal(recs[0].ring, 'ring3');
44
+ // fail-open: a nonsense dir must not throw
45
+ assert.doesNotThrow(() => recordApiUsage({ usage: {} }, { watchtowerDir: '/nonexistent/\0/x' }));
46
+ } finally { rmSync(wt, { recursive: true, force: true }); }
47
+ });
48
+
49
+ test('shouldRunIdlePass: first-run runs; unchanged+in-floor skips; changed runs; floor elapsed runs', () => {
50
+ const wt = tmpWt();
51
+ try {
52
+ const t0 = 1_000_000_000_000;
53
+ // never ran → run
54
+ assert.equal(shouldRunIdlePass('p', { watchtowerDir: wt, signature: 'A', nowMs: t0 }).run, true);
55
+ markIdlePassRan('p', { watchtowerDir: wt, signature: 'A', nowMs: t0 });
56
+ // same signature, within floor → skip
57
+ const g1 = shouldRunIdlePass('p', { watchtowerDir: wt, signature: 'A', nowMs: t0 + 60_000 });
58
+ assert.equal(g1.run, false);
59
+ assert.equal(g1.reason, 'idle-skip');
60
+ // changed signature → run
61
+ assert.equal(shouldRunIdlePass('p', { watchtowerDir: wt, signature: 'B', nowMs: t0 + 60_000 }).reason, 'activity');
62
+ // floor elapsed → run
63
+ const g3 = shouldRunIdlePass('p', { watchtowerDir: wt, signature: 'A', nowMs: t0 + RING2_IDLE_FLOOR_MS + 1 });
64
+ assert.equal(g3.run, true);
65
+ assert.equal(g3.reason, 'idle-floor');
66
+ } finally { rmSync(wt, { recursive: true, force: true }); }
67
+ });
68
+
69
+ test('computeActivitySignature: stable when nothing changes, reflects ring3-health', () => {
70
+ const wt = tmpWt();
71
+ try {
72
+ const s1 = computeActivitySignature(wt); // no files → empty-ish, must not throw
73
+ assert.equal(typeof s1, 'string');
74
+ // writing a ring3-health changes the signature
75
+ mkdirSync(join(wt, 'state'), { recursive: true });
76
+ writeFileSync(join(wt, 'state', 'ring3-health.json'), JSON.stringify({ last_run: '2026-07-12T00:00:00Z', session_id: 'abc' }));
77
+ const s2 = computeActivitySignature(wt);
78
+ assert.notEqual(s1, s2);
79
+ assert.equal(computeActivitySignature(wt), s2, 'stable when unchanged');
80
+ } finally { rmSync(wt, { recursive: true, force: true }); }
81
+ });
@@ -0,0 +1,47 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { loadBetterSqlite3, verifyDatabaseConstructor } from '../watchtower-lib.mjs';
5
+
6
+ // better-sqlite3 loads its native addon lazily at the first `new Database()`,
7
+ // not at require() — so a candidate that resolves to an ABI-mismatched copy
8
+ // (e.g. a stray ~/node_modules install built under a different node) passes a
9
+ // bare require and then explodes in the caller. verifyDatabaseConstructor
10
+ // forces the addon load inside the loader's per-candidate try so the
11
+ // candidate chain falls through instead (act:b9414039).
12
+
13
+ test('verifyDatabaseConstructor throws when construction throws (ABI-mismatch shape)', () => {
14
+ class Broken {
15
+ constructor() {
16
+ throw new Error(
17
+ "The module 'better_sqlite3.node' was compiled against a different Node.js version"
18
+ );
19
+ }
20
+ }
21
+ assert.throws(() => verifyDatabaseConstructor(Broken), /different Node\.js version/);
22
+ });
23
+
24
+ test('verifyDatabaseConstructor returns the constructor when construction works', () => {
25
+ const opened = [];
26
+ class Working {
27
+ constructor(name) { opened.push(name); }
28
+ close() { opened.pop(); }
29
+ }
30
+ const result = verifyDatabaseConstructor(Working);
31
+ assert.equal(result, Working);
32
+ // The throwaway :memory: db was opened and closed, not leaked.
33
+ assert.deepEqual(opened, []);
34
+ });
35
+
36
+ test('loadBetterSqlite3 returns a verified, working constructor in this repo', () => {
37
+ const Database = loadBetterSqlite3(process.cwd());
38
+ assert.ok(Database, 'expected a candidate to resolve (repo node_modules has better-sqlite3)');
39
+ const db = new Database(':memory:');
40
+ try {
41
+ db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)');
42
+ db.prepare('INSERT INTO t (v) VALUES (?)').run('ok');
43
+ assert.equal(db.prepare('SELECT v FROM t').get().v, 'ok');
44
+ } finally {
45
+ db.close();
46
+ }
47
+ });