task-store-unit.test.mjs
138 lines 4.8 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Tier 1 — UNIT: Task store helpers, validation, and projections.
3 *
4 * @see lib/task/task-store.mjs
5 * @see docs/TASK-STORE-CONTRACT-2G.md §9
6 */
7 import { describe, it, beforeEach, afterEach } from 'node:test';
8 import assert from 'node:assert/strict';
9 import fs from 'node:fs';
10 import path from 'node:path';
11 import { fileURLToPath } from 'node:url';
12 import {
13 TASK_ID_RE,
14 UID_HASH_REF_RE,
15 SAFE_ARTIFACT_REF_RE,
16 validateTaskRecord,
17 taskSummaryForClient,
18 taskForClient,
19 seedStarterTasks,
20 MAX_ARTIFACT_LINKS,
21 } from '../lib/task/task-store.mjs';
22 import { loadFlowStore, getVaultFlowStore, saveFlowStore } from '../lib/flow/flow-store.mjs';
23 import { getRepoRoot } from '../lib/repo-root.mjs';
24
25 const __dirname = path.dirname(fileURLToPath(import.meta.url));
26 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-task-store-unit');
27 const starterDir = path.join(getRepoRoot(), 'tasks/starter');
28
29 const VALID_TASK = JSON.parse(
30 fs.readFileSync(path.join(starterDir, 'task_2g_handover_001.json'), 'utf8'),
31 );
32
33 describe('Task store — id regexes', () => {
34 it('TASK_ID_RE accepts canonical ids and rejects malformed', () => {
35 assert.ok(TASK_ID_RE.test('task_2g_handover_001'));
36 assert.ok(TASK_ID_RE.test('task_a'));
37 assert.ok(!TASK_ID_RE.test('task'));
38 assert.ok(!TASK_ID_RE.test('not_task_x'));
39 assert.ok(!TASK_ID_RE.test('task_UPPER'));
40 });
41
42 it('UID_HASH_REF_RE and SAFE_ARTIFACT_REF_RE enforce hygiene', () => {
43 assert.ok(UID_HASH_REF_RE.test(`uid_hash:${'a'.repeat(64)}`));
44 assert.ok(!UID_HASH_REF_RE.test('uid_hash:short'));
45 assert.ok(SAFE_ARTIFACT_REF_RE.test('note:handover-summary'));
46 assert.ok(!SAFE_ARTIFACT_REF_RE.test('note with spaces'));
47 });
48 });
49
50 describe('Task store — client projections', () => {
51 it('taskSummaryForClient drops assignee and artifact_links', () => {
52 const validated = validateTaskRecord(VALID_TASK);
53 assert.equal(validated.ok, true);
54 const summary = taskSummaryForClient(validated.task);
55 assert.equal(summary.task_id, 'task_2g_handover_001');
56 assert.equal('assignee_ref' in summary, false);
57 assert.equal('artifact_links' in summary, false);
58 assert.equal('created' in summary, false);
59 });
60
61 it('taskForClient caps artifact_links and sets truncated', () => {
62 const links = Array.from({ length: MAX_ARTIFACT_LINKS + 5 }, (_, i) => ({
63 kind: 'note',
64 ref: `note:ref_${i}`,
65 }));
66 const validated = validateTaskRecord({ ...VALID_TASK, artifact_links: links });
67 assert.equal(validated.ok, true);
68 const client = taskForClient(validated.task);
69 assert.equal(client.artifact_links.length, MAX_ARTIFACT_LINKS);
70 assert.equal(client.truncated, true);
71 });
72 });
73
74 describe('Task store — seeding and vault compat', () => {
75 const dataDir = path.join(tmpRoot, 'seed');
76 const vaultId = 'default';
77
78 beforeEach(() => {
79 fs.rmSync(tmpRoot, { recursive: true, force: true });
80 fs.mkdirSync(dataDir, { recursive: true });
81 });
82
83 afterEach(() => {
84 fs.rmSync(tmpRoot, { recursive: true, force: true });
85 });
86
87 it('seedStarterTasks validates good bundle from tasks/starter', () => {
88 const result = seedStarterTasks(dataDir, vaultId, { starterDir });
89 assert.ok(result.seeded >= 1);
90 const vault = getVaultFlowStore(dataDir, vaultId);
91 assert.ok(vault.tasks.some((t) => t.task_id === 'task_2g_handover_001'));
92 });
93
94 it('rejects schema-invalid bundle without partial write', () => {
95 const badDir = path.join(tmpRoot, 'bad-starter');
96 fs.mkdirSync(badDir, { recursive: true });
97 fs.writeFileSync(path.join(badDir, 'task_bad.json'), JSON.stringify({ schema: 'nope' }), 'utf8');
98 fs.writeFileSync(
99 path.join(badDir, 'task_good_copy.json'),
100 JSON.stringify({ ...VALID_TASK, task_id: 'task_good_copy' }),
101 'utf8',
102 );
103
104 seedStarterTasks(dataDir, vaultId, { starterDir: badDir });
105 const vault = getVaultFlowStore(dataDir, vaultId);
106 assert.equal(vault.tasks.some((t) => t.task_id === 'task_good_copy'), true);
107 assert.equal(vault.tasks.some((t) => t.task_id === 'task_bad'), false);
108 });
109
110 it('vault load defaults tasks: [] when key absent (backward compat)', () => {
111 saveFlowStore(dataDir, {
112 vaults: {
113 legacy: {
114 flows: [],
115 steps: [],
116 runs: [],
117 candidates: [],
118 projections: [],
119 },
120 },
121 });
122 const vault = getVaultFlowStore(dataDir, 'legacy');
123 assert.deepEqual(vault.tasks, []);
124 });
125 });
126
127 describe('Task store — validateTaskRecord', () => {
128 it('accepts canonical starter and rejects pointer body fields', () => {
129 const ok = validateTaskRecord(VALID_TASK);
130 assert.equal(ok.ok, true);
131
132 const bad = validateTaskRecord({
133 ...VALID_TASK,
134 artifact_links: [{ kind: 'note', ref: 'note:x', body: 'secret' }],
135 });
136 assert.equal(bad.ok, false);
137 });
138 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago