1/**
2 * Tests for project-local Impeccable path resolution.
3 * Run with: node --test tests/impeccable-paths.test.mjs
4 */
5
6import { describe, it, beforeEach, afterEach } from 'node:test';
7import assert from 'node:assert/strict';
8import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9import { join } from 'node:path';
10import { tmpdir } from 'node:os';
11
12import {
13 getDesignSidecarPath,
14 getLegacyLiveServerPath,
15 getLiveAnnotationsDir,
16 getLiveConfigPath,
17 getLiveServerPath,
18 getLiveSessionsDir,
19 readLiveServerInfo,
20 resolveDesignSidecarPath,
21 resolveLiveConfigPath,
22} from '../skill/scripts/impeccable-paths.mjs';
23
24describe('impeccable project paths', () => {
25 let tmp;
26
27 beforeEach(() => {
28 tmp = mkdtempSync(join(tmpdir(), 'impeccable-paths-'));
29 });
30
31 afterEach(() => {
32 rmSync(tmp, { recursive: true, force: true });
33 });
34
35 it('resolves the generated design sidecar under .impeccable', () => {
36 assert.equal(getDesignSidecarPath(tmp), join(tmp, '.impeccable', 'design.json'));
37
38 mkdirSync(join(tmp, '.impeccable'), { recursive: true });
39 writeFileSync(join(tmp, 'DESIGN.json'), '{"source":"legacy"}');
40 writeFileSync(getDesignSidecarPath(tmp), '{"source":"new"}');
41
42 assert.equal(resolveDesignSidecarPath(tmp), getDesignSidecarPath(tmp));
43 });
44
45 it('falls back to legacy root DESIGN.json when the new sidecar is missing', () => {
46 const legacyPath = join(tmp, 'DESIGN.json');
47 writeFileSync(legacyPath, '{"source":"legacy"}');
48
49 assert.equal(resolveDesignSidecarPath(tmp), legacyPath);
50 });
51
52 it('uses .impeccable/live/config.json as the default live config path', () => {
53 assert.equal(resolveLiveConfigPath({ cwd: tmp, scriptsDir: join(tmp, 'scripts'), env: {} }), getLiveConfigPath(tmp));
54 });
55
56 it('falls back to legacy scripts/config.json when no new live config exists', () => {
57 const scriptsDir = join(tmp, 'skills', 'impeccable', 'scripts');
58 mkdirSync(scriptsDir, { recursive: true });
59 const legacyConfig = join(scriptsDir, 'config.json');
60 writeFileSync(legacyConfig, '{"files":["index.html"]}');
61
62 assert.equal(resolveLiveConfigPath({ cwd: tmp, scriptsDir, env: {} }), legacyConfig);
63 });
64
65 it('lets IMPECCABLE_LIVE_CONFIG override both new and legacy config locations', () => {
66 const override = join(tmp, 'custom-live-config.json');
67 mkdirSync(join(tmp, '.impeccable', 'live'), { recursive: true });
68 writeFileSync(getLiveConfigPath(tmp), '{"source":"new"}');
69 writeFileSync(override, '{"source":"override"}');
70
71 assert.equal(
72 resolveLiveConfigPath({ cwd: tmp, scriptsDir: join(tmp, 'scripts'), env: { IMPECCABLE_LIVE_CONFIG: 'custom-live-config.json' } }),
73 override,
74 );
75 });
76
77 it('places live server, session, and annotation state under .impeccable/live', () => {
78 assert.equal(getLiveServerPath(tmp), join(tmp, '.impeccable', 'live', 'server.json'));
79 assert.equal(getLiveSessionsDir(tmp), join(tmp, '.impeccable', 'live', 'sessions'));
80 assert.equal(getLiveAnnotationsDir(tmp), join(tmp, '.impeccable', 'live', 'annotations'));
81 });
82
83 it('reads new live server state before legacy recovery state', () => {
84 mkdirSync(join(tmp, '.impeccable', 'live'), { recursive: true });
85 writeFileSync(getLiveServerPath(tmp), JSON.stringify({ port: 8401, token: 'new' }));
86 writeFileSync(getLegacyLiveServerPath(tmp), JSON.stringify({ port: 8400, token: 'legacy' }));
87
88 const record = readLiveServerInfo(tmp);
89 assert.equal(record.path, getLiveServerPath(tmp));
90 assert.equal(record.info.token, 'new');
91 });
92
93 it('reads legacy live server state when the new state file is absent', () => {
94 writeFileSync(getLegacyLiveServerPath(tmp), JSON.stringify({ port: 8400, token: 'legacy' }));
95
96 const record = readLiveServerInfo(tmp);
97 assert.equal(record.path, getLegacyLiveServerPath(tmp));
98 assert.equal(record.info.token, 'legacy');
99 });
100});