@mmnto/cli 2.4.0 → 2.6.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.
- package/dist/commands/agents-floor-sterility.test.d.ts +25 -0
- package/dist/commands/agents-floor-sterility.test.d.ts.map +1 -0
- package/dist/commands/agents-floor-sterility.test.js +217 -0
- package/dist/commands/agents-floor-sterility.test.js.map +1 -0
- package/dist/commands/config-drift.test.js +5 -3
- package/dist/commands/config-drift.test.js.map +1 -1
- package/dist/commands/eject.d.ts +20 -0
- package/dist/commands/eject.d.ts.map +1 -1
- package/dist/commands/eject.js +131 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +280 -2
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/init-templates.d.ts +74 -2
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +298 -6
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.d.ts +36 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +172 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +380 -3
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/signoff-table-sync.test.d.ts +2 -0
- package/dist/commands/signoff-table-sync.test.d.ts.map +1 -0
- package/dist/commands/signoff-table-sync.test.js +126 -0
- package/dist/commands/signoff-table-sync.test.js.map +1 -0
- package/package.json +2 -2
|
@@ -3,12 +3,25 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest';
|
|
6
|
+
// Failure-injection seam for the shared atomic writer, the same shape
|
|
7
|
+
// eject.test.ts carries: passthrough to the REAL helper unless a test arms a
|
|
8
|
+
// failure, so every fixture keeps exercising real temp-file-and-rename writes.
|
|
9
|
+
const atomicControl = {};
|
|
10
|
+
vi.mock('@mmnto/totem/fs-atomic', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal();
|
|
12
|
+
const writeFileAtomicSync = ((...args) => {
|
|
13
|
+
if (atomicControl.failAll)
|
|
14
|
+
throw atomicControl.failAll;
|
|
15
|
+
return actual.writeFileAtomicSync(...args);
|
|
16
|
+
});
|
|
17
|
+
return { ...actual, writeFileAtomicSync };
|
|
18
|
+
});
|
|
6
19
|
import { AUTO_CLOSE_REGEX_SOURCE, LedgerEventSchema, resolveSelfAgents } from '@mmnto/totem';
|
|
7
20
|
import { UNIVERSAL_BASELINE_LESSONS, UNIVERSAL_BASELINE_MARKDOWN, UNIVERSAL_BASELINE_MARKER, } from '../assets/universal-baseline.js';
|
|
8
21
|
import { cleanTmpDir } from '../test-utils.js';
|
|
9
|
-
import { buildNpxCommand, detectEmbeddingTier, detectReflexStatus, findUnownedHookSibling, generateConfig, initCommand, installBaselineLessons, installGeminiHooks, OLLAMA_FLOOR_DEFAULT_BASE_URL, probeOllamaFloor, REFLEX_VERSION, resolveToolSelection, scaffoldClaudeHooks, scaffoldClaudeSessionStart, scaffoldClaudeSkill, scaffoldClaudeWriteShield, scaffoldFile, scaffoldMcpConfig, upgradeReflexes, } from './init.js';
|
|
22
|
+
import { buildNpxCommand, deriveProjectName, detectEmbeddingTier, detectReflexStatus, findUnownedHookSibling, generateConfig, initCommand, installBaselineLessons, installGeminiHooks, OLLAMA_FLOOR_DEFAULT_BASE_URL, probeOllamaFloor, REFLEX_VERSION, resolveToolSelection, scaffoldAgentsFloor, scaffoldClaudeHooks, scaffoldClaudeSessionStart, scaffoldClaudeSkill, scaffoldClaudeWriteShield, scaffoldFile, scaffoldMcpConfig, upgradeReflexes, } from './init.js';
|
|
10
23
|
import { detectProject } from './init-detect.js';
|
|
11
|
-
import { AI_PROMPT_BLOCK, BARE_REF_REGEX_SOURCE, CLAUDE_PREWRITESHIELD, CLAUDE_PREWRITESHIELD_ENTRY, CLAUDE_SESSION_START, CLAUDE_SESSION_START_ENTRY, DISTRIBUTED_CLAUDE_SKILLS, GEMINI_BEFORE_TOOL, GEMINI_SESSION_START, generateConfigForFormat, REVIEW_LOOP_SKILL_CONTENT, REVIEW_REPLY_SKILL_CONTENT, SIGNOFF_SKILL_CONTENT, SIGNON_SKILL_CONTENT, SKILL_MARKER_END, SKILL_MARKER_START, TOTEM_FILE_END, TOTEM_FILE_MARKER, } from './init-templates.js';
|
|
24
|
+
import { AGENTS_FLOOR_BLOCK, AGENTS_FLOOR_END, AGENTS_FLOOR_START, AI_PROMPT_BLOCK, BARE_REF_REGEX_SOURCE, CLAUDE_PREWRITESHIELD, CLAUDE_PREWRITESHIELD_ENTRY, CLAUDE_SESSION_START, CLAUDE_SESSION_START_ENTRY, DISTRIBUTED_CLAUDE_SKILLS, GEMINI_BEFORE_TOOL, GEMINI_SESSION_START, generateConfigForFormat, renderAgentsFloorScaffold, REVIEW_LOOP_SKILL_CONTENT, REVIEW_REPLY_SKILL_CONTENT, SIGNOFF_SKILL_CONTENT, SIGNON_SKILL_CONTENT, SKILL_MARKER_END, SKILL_MARKER_START, TOTEM_FILE_END, TOTEM_FILE_MARKER, } from './init-templates.js';
|
|
12
25
|
const SERVER_ENTRY = { type: 'stdio', command: 'npx', args: ['-y', '@mmnto/mcp'] };
|
|
13
26
|
describe('buildNpxCommand', () => {
|
|
14
27
|
it('returns cmd /c npx on Windows', () => {
|
|
@@ -3273,8 +3286,14 @@ describe('Distributed skill constants match source-of-truth (mmnto-ai/totem#1890
|
|
|
3273
3286
|
// The dry/apply pair was already pinned by the two `toContain` fences a few
|
|
3274
3287
|
// lines above, so that ordering assertion could not have gone vacuous — the
|
|
3275
3288
|
// guard below is belt-and-braces, not a repair.
|
|
3289
|
+
//
|
|
3290
|
+
// The needle is the EXECUTABLE fence, not the first mention of the verb:
|
|
3291
|
+
// since mmnto-ai/totem#2861 the step-2 paragraph names `totem
|
|
3292
|
+
// resolve-threads` before the post step as the source of the root comment
|
|
3293
|
+
// id each disposition line carries — a reference, not a step. The ordering
|
|
3294
|
+
// this test pins is that the STEP runs after the comment is posted.
|
|
3276
3295
|
const postAt = section.indexOf('gh pr comment $ARGUMENTS --body-file -');
|
|
3277
|
-
const resolveAt = section.indexOf('
|
|
3296
|
+
const resolveAt = section.indexOf('```bash\ntotem resolve-threads $ARGUMENTS\n```');
|
|
3278
3297
|
expect(postAt).toBeGreaterThanOrEqual(0);
|
|
3279
3298
|
expect(resolveAt).toBeGreaterThanOrEqual(0);
|
|
3280
3299
|
expect(postAt).toBeLessThan(resolveAt);
|
|
@@ -3286,4 +3305,362 @@ describe('Distributed skill constants match source-of-truth (mmnto-ai/totem#1890
|
|
|
3286
3305
|
expect(dryAt).toBeLessThan(applyAt);
|
|
3287
3306
|
});
|
|
3288
3307
|
});
|
|
3308
|
+
// ─── The public AGENTS.md floor (mmnto-ai/totem-strategy#619) ─────────
|
|
3309
|
+
describe('deriveProjectName', () => {
|
|
3310
|
+
let tmpDir;
|
|
3311
|
+
beforeEach(() => {
|
|
3312
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-floor-name-'));
|
|
3313
|
+
});
|
|
3314
|
+
afterEach(() => {
|
|
3315
|
+
cleanTmpDir(tmpDir);
|
|
3316
|
+
});
|
|
3317
|
+
it('strips the npm scope from package.json name', () => {
|
|
3318
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"@acme/widgets"}');
|
|
3319
|
+
expect(deriveProjectName(tmpDir)).toBe('widgets');
|
|
3320
|
+
});
|
|
3321
|
+
it('keeps an unscoped name as written', () => {
|
|
3322
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"widgets"}');
|
|
3323
|
+
expect(deriveProjectName(tmpDir)).toBe('widgets');
|
|
3324
|
+
});
|
|
3325
|
+
it('falls back to the directory basename without a package.json, or with an unparseable or nameless one', () => {
|
|
3326
|
+
expect(deriveProjectName(tmpDir)).toBe(path.basename(tmpDir));
|
|
3327
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{not json');
|
|
3328
|
+
expect(deriveProjectName(tmpDir)).toBe(path.basename(tmpDir));
|
|
3329
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":" "}');
|
|
3330
|
+
expect(deriveProjectName(tmpDir)).toBe(path.basename(tmpDir));
|
|
3331
|
+
});
|
|
3332
|
+
it('never returns an empty title and never mints a second heading', () => {
|
|
3333
|
+
// A scope with nothing after it strips to nothing — fall through to the basename.
|
|
3334
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"@acme/"}');
|
|
3335
|
+
expect(deriveProjectName(tmpDir)).toBe(path.basename(tmpDir));
|
|
3336
|
+
// Whitespace, a newline included, collapses to one space on the heading line.
|
|
3337
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"evil\\n# Injected Heading"}');
|
|
3338
|
+
expect(deriveProjectName(tmpDir)).toBe('evil # Injected Heading');
|
|
3339
|
+
expect(renderAgentsFloorScaffold(deriveProjectName(tmpDir)).split('\n')[0]).toBe('# evil # Injected Heading: Agent Instructions');
|
|
3340
|
+
});
|
|
3341
|
+
});
|
|
3342
|
+
describe('scaffoldAgentsFloor', () => {
|
|
3343
|
+
let tmpDir;
|
|
3344
|
+
beforeEach(() => {
|
|
3345
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-floor-'));
|
|
3346
|
+
});
|
|
3347
|
+
afterEach(() => {
|
|
3348
|
+
cleanTmpDir(tmpDir);
|
|
3349
|
+
});
|
|
3350
|
+
const agentsPath = () => path.join(tmpDir, 'AGENTS.md');
|
|
3351
|
+
it('creates the whole scaffold when no AGENTS.md exists', () => {
|
|
3352
|
+
expect(scaffoldAgentsFloor(tmpDir, 'widgets')).toEqual({ action: 'created' });
|
|
3353
|
+
const written = fs.readFileSync(agentsPath(), 'utf-8');
|
|
3354
|
+
expect(written).toBe(renderAgentsFloorScaffold('widgets'));
|
|
3355
|
+
expect(written.startsWith('# widgets: Agent Instructions')).toBe(true);
|
|
3356
|
+
expect(written).toContain(AGENTS_FLOOR_BLOCK);
|
|
3357
|
+
});
|
|
3358
|
+
it('is a byte no-op on its own scaffold (unchanged)', () => {
|
|
3359
|
+
scaffoldAgentsFloor(tmpDir, 'widgets');
|
|
3360
|
+
const before = fs.readFileSync(agentsPath(), 'utf-8');
|
|
3361
|
+
expect(scaffoldAgentsFloor(tmpDir, 'widgets')).toEqual({ action: 'unchanged' });
|
|
3362
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(before);
|
|
3363
|
+
});
|
|
3364
|
+
it('refreshes exactly the span between the markers and keeps every other byte', () => {
|
|
3365
|
+
const above = '# Mine\n\nMy intro stays.\n\n';
|
|
3366
|
+
const below = '\n\n## My rules\n\n- keep this\n';
|
|
3367
|
+
const stale = `${AGENTS_FLOOR_START}\nold managed text\n${AGENTS_FLOOR_END}`;
|
|
3368
|
+
fs.writeFileSync(agentsPath(), above + stale + below, 'utf-8');
|
|
3369
|
+
expect(scaffoldAgentsFloor(tmpDir, 'ignored')).toEqual({ action: 'refreshed' });
|
|
3370
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(above + AGENTS_FLOOR_BLOCK + below);
|
|
3371
|
+
// A second pass is a no-op on the refreshed file.
|
|
3372
|
+
expect(scaffoldAgentsFloor(tmpDir, 'ignored')).toEqual({ action: 'unchanged' });
|
|
3373
|
+
});
|
|
3374
|
+
it('preserves a repository-authored AGENTS.md that carries no markers, with the adoption hint', () => {
|
|
3375
|
+
const mine = '# Mine\n\nAll of this is user content.\n';
|
|
3376
|
+
fs.writeFileSync(agentsPath(), mine, 'utf-8');
|
|
3377
|
+
const result = scaffoldAgentsFloor(tmpDir, 'widgets');
|
|
3378
|
+
expect(result.action).toBe('preserved');
|
|
3379
|
+
expect(result.err).toContain(AGENTS_FLOOR_START);
|
|
3380
|
+
expect(result.err).toContain('re-run `totem init`');
|
|
3381
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(mine);
|
|
3382
|
+
});
|
|
3383
|
+
it.each([
|
|
3384
|
+
['only a start marker', `# Mine\n${AGENTS_FLOOR_START}\ntext\n`],
|
|
3385
|
+
['only an end marker', `# Mine\ntext\n${AGENTS_FLOOR_END}\n`],
|
|
3386
|
+
['inverted markers', `# Mine\n${AGENTS_FLOOR_END}\ntext\n${AGENTS_FLOOR_START}\n`],
|
|
3387
|
+
])('preserves a file with %s byte-untouched', (_name, content) => {
|
|
3388
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3389
|
+
expect(scaffoldAgentsFloor(tmpDir, 'widgets').action).toBe('preserved');
|
|
3390
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3391
|
+
});
|
|
3392
|
+
it("the managed span carries no trailing newline, so the seam after the end marker is the file's own", () => {
|
|
3393
|
+
expect(AGENTS_FLOOR_BLOCK.startsWith(AGENTS_FLOOR_START)).toBe(true);
|
|
3394
|
+
expect(AGENTS_FLOOR_BLOCK.endsWith(AGENTS_FLOOR_END)).toBe(true);
|
|
3395
|
+
expect(AGENTS_FLOOR_BLOCK.indexOf(AGENTS_FLOOR_START)).toBe(AGENTS_FLOOR_BLOCK.lastIndexOf(AGENTS_FLOOR_START));
|
|
3396
|
+
expect(AGENTS_FLOOR_BLOCK.indexOf(AGENTS_FLOOR_END)).toBe(AGENTS_FLOOR_BLOCK.lastIndexOf(AGENTS_FLOOR_END));
|
|
3397
|
+
});
|
|
3398
|
+
// ─── Pairing and line endings (the fold of the pre-merge leg's F2/F4/F5/F16) ──
|
|
3399
|
+
it('an orphan start marker above the span never widens the refresh — the bytes between stay', () => {
|
|
3400
|
+
const content = `# Mine\n${AGENTS_FLOOR_START}\nMY OWN TEXT\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n\n## Keep\n`;
|
|
3401
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3402
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x').action).toBe('refreshed');
|
|
3403
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n${AGENTS_FLOOR_START}\nMY OWN TEXT\n${AGENTS_FLOOR_BLOCK}\n\n## Keep\n`);
|
|
3404
|
+
});
|
|
3405
|
+
it('a second complete span after the first is left alone and named in err', () => {
|
|
3406
|
+
const content = `# Mine\n\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n\nMID\n\n${AGENTS_FLOOR_START}\nother\n${AGENTS_FLOOR_END}\n`;
|
|
3407
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3408
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3409
|
+
expect(result.action).toBe('refreshed');
|
|
3410
|
+
expect(result.err).toContain('outside the managed span');
|
|
3411
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n\n${AGENTS_FLOOR_BLOCK}\n\nMID\n\n${AGENTS_FLOOR_START}\nother\n${AGENTS_FLOOR_END}\n`);
|
|
3412
|
+
});
|
|
3413
|
+
it('an orphan end marker above a complete span is skipped, not attributed', () => {
|
|
3414
|
+
const content = `# Mine\n${AGENTS_FLOOR_END}\nstill mine\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n`;
|
|
3415
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3416
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x').action).toBe('refreshed');
|
|
3417
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n${AGENTS_FLOOR_END}\nstill mine\n${AGENTS_FLOOR_BLOCK}\n`);
|
|
3418
|
+
});
|
|
3419
|
+
it('a CRLF file gets the span in CRLF, converges in one write, and stays byte-stable', () => {
|
|
3420
|
+
const crlfScaffold = renderAgentsFloorScaffold('x').replace(/\n/g, '\r\n');
|
|
3421
|
+
fs.writeFileSync(agentsPath(), crlfScaffold, 'utf-8');
|
|
3422
|
+
// Already canonical in its own line terminator: nothing to do.
|
|
3423
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'unchanged' });
|
|
3424
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(crlfScaffold);
|
|
3425
|
+
// A drifted CRLF span is refreshed WITHOUT introducing a bare LF anywhere.
|
|
3426
|
+
fs.writeFileSync(agentsPath(), crlfScaffold.replace('Never guess', 'Never GUESS'), 'utf-8');
|
|
3427
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3428
|
+
const refreshed = fs.readFileSync(agentsPath(), 'utf-8');
|
|
3429
|
+
expect(refreshed).toBe(crlfScaffold);
|
|
3430
|
+
expect(refreshed.replace(/\r\n/g, '')).not.toContain('\n');
|
|
3431
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'unchanged' });
|
|
3432
|
+
});
|
|
3433
|
+
// ─── The re-armed leg's shapes (G1, G2, G5, G10) ────────────────────
|
|
3434
|
+
it('a two-orphan file: the span is refreshed, both user texts stay, and the residue is named', () => {
|
|
3435
|
+
const content = `# Mine\n${AGENTS_FLOOR_START}\nUSER A\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\nUSER C\n${AGENTS_FLOOR_END}\ntail\n`;
|
|
3436
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3437
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3438
|
+
expect(result.action).toBe('refreshed');
|
|
3439
|
+
expect(result.err).toContain('outside the managed span');
|
|
3440
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n${AGENTS_FLOOR_START}\nUSER A\n${AGENTS_FLOOR_BLOCK}\nUSER C\n${AGENTS_FLOOR_END}\ntail\n`);
|
|
3441
|
+
});
|
|
3442
|
+
it('markers quoted inside a fenced code block are prose: the file is preserved with the adoption hint', () => {
|
|
3443
|
+
const content = `# Mine\n\nAdopt it:\n\n\`\`\`markdown\n${AGENTS_FLOOR_START}\n${AGENTS_FLOOR_END}\n\`\`\`\n\nmore mine\n`;
|
|
3444
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3445
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3446
|
+
expect(result.action).toBe('preserved');
|
|
3447
|
+
expect(result.err).toContain('is yours');
|
|
3448
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3449
|
+
});
|
|
3450
|
+
it('a real span below a fenced quotation is the one refreshed; the quotation is untouched', () => {
|
|
3451
|
+
const quoted = `\`\`\`\n${AGENTS_FLOOR_START}\n${AGENTS_FLOOR_END}\n\`\`\`\n`;
|
|
3452
|
+
const content = `# Mine\n\n${quoted}\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n`;
|
|
3453
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3454
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3455
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n\n${quoted}\n${AGENTS_FLOOR_BLOCK}\n`);
|
|
3456
|
+
});
|
|
3457
|
+
it('a span that is the only CRLF in an LF file is refreshed to LF — the terminator comes from the rest of the file', () => {
|
|
3458
|
+
const crlfBlock = AGENTS_FLOOR_BLOCK.replace(/\n/g, '\r\n').replace('Never guess', 'Never GUESS');
|
|
3459
|
+
fs.writeFileSync(agentsPath(), `# Mine\n\n${crlfBlock}\n\n## Rules\n`, 'utf-8');
|
|
3460
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3461
|
+
const out = fs.readFileSync(agentsPath(), 'utf-8');
|
|
3462
|
+
expect(out).toBe(`# Mine\n\n${AGENTS_FLOOR_BLOCK}\n\n## Rules\n`);
|
|
3463
|
+
expect(out).not.toContain('\r');
|
|
3464
|
+
});
|
|
3465
|
+
// ─── The third leg's shapes (H1, H2, H3, H4) ────────────────────────
|
|
3466
|
+
it.each([
|
|
3467
|
+
[
|
|
3468
|
+
'a stray unterminated fence above a real span',
|
|
3469
|
+
`# Mine\n\n\`\`\`sh\necho hi\n\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\ntail\n`,
|
|
3470
|
+
3,
|
|
3471
|
+
],
|
|
3472
|
+
[
|
|
3473
|
+
'markers quoted in a fence that the wrong character tries to close',
|
|
3474
|
+
`# Mine\n\n\`\`\`markdown\n${AGENTS_FLOOR_START}\nMY EXAMPLE\n${AGENTS_FLOOR_END}\n~~~\n\nafter\n`,
|
|
3475
|
+
3,
|
|
3476
|
+
],
|
|
3477
|
+
[
|
|
3478
|
+
'markers quoted in a fence whose closer is indented as code',
|
|
3479
|
+
`# Mine\n\n\`\`\`\n${AGENTS_FLOOR_START}\n${AGENTS_FLOOR_END}\n \`\`\`\n\nafter\n`,
|
|
3480
|
+
3,
|
|
3481
|
+
],
|
|
3482
|
+
])('%s is ambiguous: nothing below the fence is touched and the fence line is named', (_name, content, line) => {
|
|
3483
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3484
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3485
|
+
expect(result.action).toBe('preserved');
|
|
3486
|
+
expect(result.err).toContain(`unclosed code fence`);
|
|
3487
|
+
expect(result.err).toContain(`line ${line}`);
|
|
3488
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3489
|
+
});
|
|
3490
|
+
it('a real span above an unclosed fence is refreshed and the fence below is still named', () => {
|
|
3491
|
+
const content = `# Mine\n\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n\n\`\`\`\n${AGENTS_FLOOR_START}\nquoted\n`;
|
|
3492
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3493
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3494
|
+
expect(result.action).toBe('refreshed');
|
|
3495
|
+
expect(result.err).toContain('unclosed code fence');
|
|
3496
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n\n${AGENTS_FLOOR_BLOCK}\n\n\`\`\`\n${AGENTS_FLOOR_START}\nquoted\n`);
|
|
3497
|
+
});
|
|
3498
|
+
it.each([1, 2, 3])('marker lines indented %s space(s) still count (an HTML block, not code)', (n) => {
|
|
3499
|
+
const pad = ' '.repeat(n);
|
|
3500
|
+
const content = `# Mine\n\n${pad}${AGENTS_FLOOR_START}\nstale\n${pad}${AGENTS_FLOOR_END}\ntail\n`;
|
|
3501
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3502
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3503
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n\n${AGENTS_FLOOR_BLOCK}\ntail\n`);
|
|
3504
|
+
});
|
|
3505
|
+
it('a byte-order mark before a marker on line 1 is tolerated', () => {
|
|
3506
|
+
const bom = String.fromCharCode(0xfeff);
|
|
3507
|
+
fs.writeFileSync(agentsPath(), `${bom}${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\ntail\n`, 'utf-8');
|
|
3508
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3509
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`${bom}${AGENTS_FLOOR_BLOCK}\ntail\n`);
|
|
3510
|
+
});
|
|
3511
|
+
it('the unpaired-marker hint says remove, never pair, and carries the contract', () => {
|
|
3512
|
+
fs.writeFileSync(agentsPath(), `# Mine\n${AGENTS_FLOOR_START}\nUSER TEXT I CARE ABOUT\n`, 'utf-8');
|
|
3513
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3514
|
+
expect(result.action).toBe('preserved');
|
|
3515
|
+
expect(result.err).toContain('Remove it');
|
|
3516
|
+
expect(result.err).not.toContain('start above end');
|
|
3517
|
+
expect(result.err).toContain('a managed span by definition');
|
|
3518
|
+
});
|
|
3519
|
+
// ─── The fifth leg's shapes (K1 tab fences, K2 joined hints, K3 fresh line numbers) ──
|
|
3520
|
+
it('a tab-indented fence line is indented code, not a fence: a closed quotation below it stays prose', () => {
|
|
3521
|
+
const content = `# Mine\n\n\t\`\`\`\n\n\`\`\`markdown\n${AGENTS_FLOOR_START}\nMY EXAMPLE LINE\n${AGENTS_FLOOR_END}\n\`\`\`\n\nafter\n`;
|
|
3522
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3523
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3524
|
+
expect(result.action).toBe('preserved');
|
|
3525
|
+
expect(result.err).toContain('is yours');
|
|
3526
|
+
expect(result.fenceLine).toBeUndefined();
|
|
3527
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3528
|
+
});
|
|
3529
|
+
it('the fence line named after a refresh is the line in the file left on disk', () => {
|
|
3530
|
+
const content = `# Mine\n\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n\n\`\`\`\n${AGENTS_FLOOR_START}\nquoted\n`;
|
|
3531
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3532
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3533
|
+
expect(result.action).toBe('refreshed');
|
|
3534
|
+
const written = fs.readFileSync(agentsPath(), 'utf-8');
|
|
3535
|
+
const fenceLineOnDisk = written.split('\n').findIndex((l) => l === '```') + 1;
|
|
3536
|
+
expect(result.fenceLine).toBe(fenceLineOnDisk);
|
|
3537
|
+
expect(result.err).toContain(`opened at line ${fenceLineOnDisk}`);
|
|
3538
|
+
});
|
|
3539
|
+
it('a stray marker above an unclosed fence gets both hints: the contract and the fence', () => {
|
|
3540
|
+
const content = `# Mine\n${AGENTS_FLOOR_START}\nstray above\n\n\`\`\`\n${AGENTS_FLOOR_END}\nbelow\n`;
|
|
3541
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3542
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3543
|
+
expect(result.action).toBe('preserved');
|
|
3544
|
+
expect(result.err).toContain('a managed span by definition');
|
|
3545
|
+
expect(result.err).toContain('unclosed code fence');
|
|
3546
|
+
expect(result.fenceLine).toBe(5);
|
|
3547
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3548
|
+
});
|
|
3549
|
+
// ─── The bot round (greptile P1 ×3, CodeRabbit closer rule) ─────────
|
|
3550
|
+
it('a file that is not valid UTF-8 is never rewritten — preserved with the encoding hint, bytes untouched', () => {
|
|
3551
|
+
const raw = Buffer.concat([
|
|
3552
|
+
Buffer.from('# Mine ', 'utf-8'),
|
|
3553
|
+
Buffer.from([0xff, 0xfe, 0x0a]),
|
|
3554
|
+
Buffer.from(`${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n`, 'utf-8'),
|
|
3555
|
+
]);
|
|
3556
|
+
fs.writeFileSync(agentsPath(), raw);
|
|
3557
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3558
|
+
expect(result.action).toBe('preserved');
|
|
3559
|
+
expect(result.err).toContain('not valid UTF-8');
|
|
3560
|
+
expect(Buffer.compare(fs.readFileSync(agentsPath()), raw)).toBe(0);
|
|
3561
|
+
});
|
|
3562
|
+
it('a write failure leaves the original bytes intact and reports preserved with the error — the atomic writer is the seam', () => {
|
|
3563
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'created' });
|
|
3564
|
+
const drifted = fs.readFileSync(agentsPath(), 'utf-8').replace('Never guess', 'Never GUESS');
|
|
3565
|
+
fs.writeFileSync(agentsPath(), drifted);
|
|
3566
|
+
atomicControl.failAll = new Error('EACCES: simulated');
|
|
3567
|
+
try {
|
|
3568
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3569
|
+
expect(result.action).toBe('preserved');
|
|
3570
|
+
expect(result.err).toContain('[Totem Error]');
|
|
3571
|
+
expect(result.err).toContain('EACCES: simulated');
|
|
3572
|
+
// Nothing was truncated: the drifted bytes are exactly as written, and
|
|
3573
|
+
// no temp file was left beside them.
|
|
3574
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(drifted);
|
|
3575
|
+
expect(fs.readdirSync(tmpDir)).toEqual(['AGENTS.md']);
|
|
3576
|
+
}
|
|
3577
|
+
finally {
|
|
3578
|
+
atomicControl.failAll = undefined;
|
|
3579
|
+
}
|
|
3580
|
+
// With the seam disarmed the same refresh goes through.
|
|
3581
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3582
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).not.toContain('Never GUESS');
|
|
3583
|
+
});
|
|
3584
|
+
it('a closing fence never carries an info string: a ```sh line leaves the quotation open, so it is ambiguous', () => {
|
|
3585
|
+
const content = `# Mine\n\n\`\`\`\n${AGENTS_FLOOR_START}\nQUOTED\n${AGENTS_FLOOR_END}\n\`\`\`sh\necho\n`;
|
|
3586
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3587
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3588
|
+
expect(result.action).toBe('preserved');
|
|
3589
|
+
expect(result.err).toContain('unclosed code fence');
|
|
3590
|
+
expect(result.fenceLine).toBe(3);
|
|
3591
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3592
|
+
});
|
|
3593
|
+
it('a fence-looking line inside an HTML comment is raw HTML: the closed quotation below it stays prose', () => {
|
|
3594
|
+
const content = `# Mine\n\n<!--\n\`\`\`\n-->\n\n\`\`\`markdown\n${AGENTS_FLOOR_START}\nMY EXAMPLE\n${AGENTS_FLOOR_END}\n\`\`\`\n\nafter\n`;
|
|
3595
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3596
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3597
|
+
expect(result.action).toBe('preserved');
|
|
3598
|
+
expect(result.err).toContain('is yours');
|
|
3599
|
+
expect(result.fenceLine).toBeUndefined();
|
|
3600
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3601
|
+
});
|
|
3602
|
+
it('a comment opener inside a fenced block is the fence content: the fence still closes and the real span below is refreshed', () => {
|
|
3603
|
+
const content = `# Mine\n\n\`\`\`html\n<!-- example opener\n\`\`\`\n\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n\nafter\n`;
|
|
3604
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3605
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3606
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n\n\`\`\`html\n<!-- example opener\n\`\`\`\n\n${AGENTS_FLOOR_BLOCK}\n\nafter\n`);
|
|
3607
|
+
});
|
|
3608
|
+
it('a multi-line comment that reaches a marker swallows it: ambiguous, named, nothing touched', () => {
|
|
3609
|
+
// The stray `<!--` runs to the first `-->`, which is the start marker's own.
|
|
3610
|
+
const content = `# Mine\n\n<!-- TODO revisit\n\n\`\`\`\n${AGENTS_FLOOR_START}\nQUOTED EXAMPLE\n${AGENTS_FLOOR_END}\n\`\`\`\n\nafter\n`;
|
|
3611
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3612
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3613
|
+
expect(result.action).toBe('preserved');
|
|
3614
|
+
expect(result.err).toContain('HTML comment');
|
|
3615
|
+
expect(result.fenceLine).toBe(3);
|
|
3616
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3617
|
+
});
|
|
3618
|
+
it('wrapping the span in an HTML comment is named, never silently managed or silently ignored', () => {
|
|
3619
|
+
const content = `# Mine\n\n<!--\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\n-->\n\nafter\n`;
|
|
3620
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3621
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3622
|
+
expect(result.action).toBe('preserved');
|
|
3623
|
+
expect(result.fenceLine).toBe(3);
|
|
3624
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3625
|
+
});
|
|
3626
|
+
it.each([
|
|
3627
|
+
[
|
|
3628
|
+
'markers quoted in inline code spans mid-sentence',
|
|
3629
|
+
`# Mine\n\nAdd \`${AGENTS_FLOOR_START}\` and \`${AGENTS_FLOOR_END}\` to adopt.\n\nmore\n`,
|
|
3630
|
+
],
|
|
3631
|
+
[
|
|
3632
|
+
'markers in an indented code block',
|
|
3633
|
+
`# Mine\n\n ${AGENTS_FLOOR_START}\n ${AGENTS_FLOOR_END}\n\nmore\n`,
|
|
3634
|
+
],
|
|
3635
|
+
[
|
|
3636
|
+
'a marker that shares its line with prose',
|
|
3637
|
+
`# Mine\nkeep ${AGENTS_FLOOR_START}\ntext\n${AGENTS_FLOOR_END} more\n`,
|
|
3638
|
+
],
|
|
3639
|
+
])('%s are prose: the file is preserved with the adoption hint', (_name, content) => {
|
|
3640
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3641
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3642
|
+
expect(result.action).toBe('preserved');
|
|
3643
|
+
expect(result.err).toContain('is yours');
|
|
3644
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(content);
|
|
3645
|
+
});
|
|
3646
|
+
it('a marker line with trailing blanks still counts, and the blanks leave with the span', () => {
|
|
3647
|
+
const content = `# Mine\n\n${AGENTS_FLOOR_START} \nstale\n${AGENTS_FLOOR_END}\t\n`;
|
|
3648
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3649
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'refreshed' });
|
|
3650
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(`# Mine\n\n${AGENTS_FLOOR_BLOCK}\n`);
|
|
3651
|
+
});
|
|
3652
|
+
it('a file that is nothing but a CRLF span keeps its own endings (byte no-op)', () => {
|
|
3653
|
+
const crlf = AGENTS_FLOOR_BLOCK.replace(/\n/g, '\r\n');
|
|
3654
|
+
fs.writeFileSync(agentsPath(), crlf, 'utf-8');
|
|
3655
|
+
expect(scaffoldAgentsFloor(tmpDir, 'x')).toEqual({ action: 'unchanged' });
|
|
3656
|
+
expect(fs.readFileSync(agentsPath(), 'utf-8')).toBe(crlf);
|
|
3657
|
+
});
|
|
3658
|
+
it('the stray-marker hint says why: a start followed by an end is a span by definition', () => {
|
|
3659
|
+
const content = `# Mine\n${AGENTS_FLOOR_START}\nUSER A\n${AGENTS_FLOOR_START}\nstale\n${AGENTS_FLOOR_END}\nUSER C\n${AGENTS_FLOOR_END}\n`;
|
|
3660
|
+
fs.writeFileSync(agentsPath(), content, 'utf-8');
|
|
3661
|
+
const result = scaffoldAgentsFloor(tmpDir, 'x');
|
|
3662
|
+
expect(result.action).toBe('refreshed');
|
|
3663
|
+
expect(result.err).toContain('a managed span by definition');
|
|
3664
|
+
});
|
|
3665
|
+
});
|
|
3289
3666
|
//# sourceMappingURL=init.test.js.map
|